Otrzymujesz tablicę elementów
Zadanie polega na przeplataniu tablicy przy użyciu algorytmu lokalnego, takiego jak tablica wynikowa
Gdyby nie istniał wymóg lokalny, moglibyśmy łatwo utworzyć nową tablicę i skopiować elementy, dając algorytm czasowy .
Z wymaganiem na miejscu algorytm dzielenia i zdobywania powoduje, że algorytm jest .
Pytanie brzmi:
Czy istnieje algorytm czasowy , który również istnieje?
(Uwaga: możesz założyć model WORD RAM o jednolitym koszcie, więc w miejscu przekłada się na ograniczenie przestrzeni).
algorithms
arrays
permutations
in-place
Aryabhata
źródło
źródło
Odpowiedzi:
Oto odpowiedź opracowana w oparciu o algorytm z artykułu, do którego link dołączył Joe: http://arxiv.org/abs/0805.1598
Najpierw zastanówmy się nad algorytmem , który wykorzystuje dziel i zwyciężaj.Θ(nlogn)
1) Dziel i rządź
Dano nam
Teraz, aby użyć dzielenia i podbijania, dla niektórych , staramy się uzyskać tablicęm=Θ(n)
i powtórzyć.
Zauważ, że część to cykliczne przesunięcieb1,b2,…bm,am+1,…an
przez miejsc.m
Jest to klasyk i można tego dokonać na miejscu poprzez trzy cofnięcia w czasie .O(n)
Zatem dziel i rządź daje ci algorytm z rekurencją podobną do .Θ(nlogn) T(n)=2T(n/2)+Θ(n)
2) Cykle permutacji
Teraz innym podejściem do problemu jest rozważenie permutacji jako zestawu rozłącznych cykli.
Permutacja jest podawana przez (przy założeniu, że od )1
Gdybyśmy w jakiś sposób wiedzieli dokładnie, czym są cykle, wykorzystując stałą dodatkową przestrzeń, moglibyśmy zrealizować permutację, wybierając element , określić, dokąd ten element idzie (używając powyższej formuły), umieścić element w docelowej lokalizacji w przestrzeni tymczasowej, umieścić element do tej docelowej lokalizacji i kontynuuj cykl. Gdy skończymy z jednym cyklem, przechodzimy do elementu następnego cyklu i podążamy za nim i tak dalej.A A
To dałoby nam algorytm czasowy , ale zakłada, że „jakoś wiedzieliśmy, jakie dokładnie są cykle” i staramy się wykonywać tę księgowość w ramach ograniczenia przestrzeni to sprawia, że ten problem jest trudny.O(n) O(1)
W tym miejscu papier wykorzystuje teorię liczb.
Można wykazać, że w przypadku, gdy , elementy w pozycjach , są w różnych cyklach i każdy cykl zawiera element w pozycji .2n+1=3k 1 3,32,…,3k−1 3m,m≥0
Wykorzystuje to fakt, że jest generatorem .2 (Z/3k)∗
Tak więc, gdy , podążanie za cyklem daje nam algorytm czasowy , ponieważ dla każdego cyklu wiemy dokładnie, od czego zacząć: potęgi (w tym ) (te można obliczyć w spacji ).2n+1=3k O(n) 3 1 O(1)
3) Ostateczny algorytm
Teraz łączymy dwa powyższe: Divide and Conquer + Permutation Cycles.
Wykonujemy dzielenie i podbijać, ale wybrać tak, jest potęgą i .m 2m+1 3 m=Θ(n)
Dlatego zamiast rekurencji na obu „połówkach”, powtarzamy tylko na jednej i wykonujemy dodatkową pracę.Θ(n)
To daje nam powtarzalność (dla niektórych ), a tym samym daje nam czas , algorytm kosmiczny!T(n)=T(cn)+Θ(n) 0<c<1 O(n) O(1)
źródło
Jestem prawie pewien, że znalazłem algorytm, który nie opiera się na teorii liczb lub teorii cyklu. Pamiętaj, że jest kilka szczegółów do uzgodnienia (być może jutro), ale jestem pewien, że się uda. Macham ręką, bo mam spać, nie dlatego, że próbuję ukryć problemy :)
Niech
A
będzie pierwszą tablicą,B
drugą,|A| = |B| = N
iN=2^k
dla niektórych załóżmyk
dla uproszczenia. NiechA[i..j]
będzie podrzędnąA
z indeksamii
aż doj
włącznie. Tablice są oparte na 0. NiechRightmostBitPos(i)
zwróci pozycję (w oparciu o 0) najbardziej wysuniętego w prawo bitu, czyli „1”i
, licząc od prawej. Algorytm działa w następujący sposób.Weźmy tablicę 16 liczb i zacznijmy przeplatać je za pomocą swapów i zobaczmy, co się stanie:
Szczególnie interesująca jest pierwsza część drugiej tablicy:
Wzór powinien być wyraźny: naprzemiennie dodajemy liczbę na końcu i zastępujemy najniższą liczbę wysoką liczbą. Pamiętaj, że zawsze dodajemy liczbę, która jest o jeden wyższa od najwyższej liczby, którą już mamy. Gdybyśmy byli w stanie dowiedzieć się, która liczba jest najniższa w danym momencie, możemy to zrobić z łatwością.
Teraz szukamy większych przykładów, aby zobaczyć, czy możemy zobaczyć wzór. Zauważ, że nie musimy poprawiać rozmiaru tablicy, aby zbudować powyższy przykład. W pewnym momencie otrzymujemy tę konfigurację (druga linia odejmuje 16 od każdej liczby):
Teraz wyraźnie pokazuje to wzór: „1 3 5 7 9 11 13 15” są 2 osobno, „2 6 10 14” są 4 osobno, a „4 12” są 8 osobno. Możemy zatem opracować algorytm, który mówi nam, jaka będzie następna najmniejsza liczba: mechanizm jest dokładnie taki, jak działają liczby binarne. Masz trochę za ostatnią połowę tablicy, trochę za drugi kwartał i tak dalej.
Jeśli więc mamy wystarczającą ilość miejsca do przechowywania tych bitów (potrzebujemy bitów, ale nasz model obliczeniowy na to pozwala - wskaźnik do tablicy również potrzebuje bitów), możemy dowiedzieć się, którą liczbę zamienić w zamortyzowany czas.log n O ( 1 )logn logn O(1)
Możemy zatem wprowadzić pierwszą połowę tablicy do jej stanu przeplotu w czasie i . Musimy jednak naprawić drugą połowę naszej tablicy, która wydaje się całkowicie pomieszana („8 6 5 7 13 14 15 16”).O ( n )O(n) O(n)
Teraz, jeśli możemy „posortować” pierwszą połowę tej drugiej części, otrzymamy „5 6 7 8 13 14 15 16” i rekurencyjne przeplatanie tej połowy załatwi sprawę: przeplatamy tablicę w time ( wywołania rekurencyjne, z których każde zmniejsza o połowę wielkość wejściową). Zauważ, że nie potrzebujemy stosu, ponieważ te wywołania są rekurencyjne, więc nasze użycie przestrzeni pozostaje .O ( log n ) O ( 1 )O(n) O(logn) O(1)
Teraz pytanie brzmi: czy jest jakiś wzór w części, którą musimy posortować? Próbowanie 32 liczb daje nam „16 12 10 14 9 11 13 15” do naprawienia. Pamiętaj, że mamy tutaj dokładnie ten sam wzór! „9 11 13 15”, „10 14” i „12” są zgrupowane w taki sam sposób, jak widzieliśmy wcześniej.
Teraz sztuczka polega na rekurencyjnym przeplataniu tych części. Przeplatamy „16” i „12” na „12 16”. Przeplatamy „12 16” i „10 14” na „10 12 14 16”. Przeplatamy „10 12 14 16” i „9 11 13 15” na „9 10 11 12 13 14 15 16”. To sortuje pierwszą część.
Podobnie jak powyżej, całkowity koszt tej operacji wynosi . Podsumowując, nadal udaje nam się uzyskać całkowity czas działania .O ( n )O(n) O(n)
Przykład:
źródło
Oto nierekurencyjny w miejscu algorytm liniowego czasu do przeplatania dwóch połówek tablicy bez dodatkowej pamięci.
Ogólna idea jest prosta: przejdź przez pierwszą połowę tablicy od lewej do prawej, zamieniając prawidłowe wartości na swoje miejsce. W miarę postępów lewe wartości, które mają być używane, są zamieniane w miejsce zwolnione przez właściwe wartości. Jedyną sztuczką jest wymyślenie, jak je wyciągnąć ponownie.
Zaczynamy od tablicy o rozmiarze N podzielonej na 2 prawie równe połowy.
[ left_items | right_items ]
Gdy go przetwarzamy, staje się
[ placed_items | remaining_left_items| swapped_left_items | remaining_right_items]
Przestrzeń wymiany rośnie według następującego wzoru: A) powiększ przestrzeń, usuwając sąsiedni prawy przedmiot i zamieniając nowy przedmiot z lewej; B) zamień najstarszy element na nowy z lewej strony. Jeśli lewe elementy są ponumerowane 1..N, ten wzór wygląda
Sekwencja zmian indeksu to dokładnie OEIS A025480 , którą można obliczyć za pomocą prostego procesu. Dzięki temu można znaleźć lokalizację wymiany, biorąc pod uwagę tylko liczbę dodanych do tej pory elementów, która jest również indeksem aktualnie umieszczanego elementu.
To wszystkie informacje, których potrzebujemy, aby wypełnić pierwszą połowę sekwencji w czasie liniowym.
Kiedy dojdziemy do punktu środkowego, tablica będzie się
[ placed_items | swapped_left_items | remaining_right_items]
składać z trzech części: Jeśli możemy rozszyfrować zamienione elementy, zmniejszyliśmy problem do połowy rozmiaru i możemy powtórzyć.Aby rozszyfrować przestrzeń wymiany, używamy następującej właściwości: Sekwencja zbudowana przez
N
naprzemienne operacje append i swap_oldest będą zawieraćN/2
elementy, według których podano ich wiekA025480(N/2)..A025480(N-1)
. (Dzielenie liczb całkowitych, mniejsze wartości są starsze).Na przykład, jeśli lewa połowa pierwotnie zawierała wartości 1..19, wówczas przestrzeń wymiany będzie zawierać
[16, 12, 10, 14, 18, 11, 13, 15, 17, 19]
. A025480 (9..18) to[2, 5, 1, 6, 3, 7, 0, 8, 4, 9]
dokładnie lista indeksów pozycji od najstarszego do najnowszego.Możemy więc rozszyfrować naszą przestrzeń wymiany, przechodząc przez nią i zamieniając
S[i]
się niąS[ A(N/2 + i)]
. To także czas liniowy.Pozostała komplikacja polega na tym, że ostatecznie osiągniesz pozycję, w której poprawna wartość powinna mieć niższy indeks, ale została już zamieniona. Znalezienie nowej lokalizacji jest łatwe: wystarczy ponownie wykonać obliczenia indeksu, aby dowiedzieć się, gdzie wymieniono element. Konieczne może być wykonanie kilku kroków, aż znajdziesz niezamienioną lokalizację.
W tym momencie połączyliśmy połowę macierzy i utrzymaliśmy porządek nie połączonych części w drugiej połowie, dokładnie
N/2 + N/4
zamieniając. Możemy kontynuować przez resztę tablicy, aby uzyskać sumęN + N/4 + N/8 + ....
swapów, która jest ściśle mniejsza niż3N/2
.Jak obliczyć A025480:
Jest to zdefiniowane w OEIS jako
a(2n) = n, a(2n+1) = a(n).
Alternatywne sformułowaniea(n) = isEven(n)? n/2 : a((n-1)/2)
. Prowadzi to do prostego algorytmu wykorzystującego operacje bitowe:Jest to amortyzowana operacja O (1) dla wszystkich możliwych wartości dla N. (1/2 potrzebuje 1 zmiany, 1/4 potrzebuje 2, 1/8 potrzebuje 3, ...) . Istnieje jeszcze szybsza metoda, która wykorzystuje małą tabelę wyszukiwania, aby znaleźć pozycję najmniej znaczącego bitu zerowego.
Biorąc to pod uwagę, oto implementacja w C:
Powinien to być algorytm dość przyjazny dla pamięci podręcznej, ponieważ dostęp do 2 z 3 lokalizacji danych jest uzyskiwany sekwencyjnie, a ilość przetwarzanych danych ściśle się zmniejsza. Metodę tę można zmienić z wymieszania na wymieszanie na wymieszanie, negując
is_even
test na początku pętli.źródło