Nietrywialny algorytm obliczania mediany okna przesuwnego

25

Muszę obliczyć medianę biegu:

  • Dane wejściowe: , , wektor .nk(x1,x2,,xn)

  • Wyjście: wektor (y1,y2,,ynk+1) , gdzie yi jest medianą (xi,xi+1,,xi+k1) .

(Brak oszustw z przybliżeniami; Chciałbym mieć dokładne rozwiązania. Elementy xi są dużymi liczbami całkowitymi.)

Istnieje prosty algorytm, który utrzymuje drzewo wyszukiwania o rozmiarze k ; całkowity czas działania wynosi O(nlogk) . („Drzewo wyszukiwania” odnosi się do wydajnej struktury danych, która obsługuje wstawianie, usuwanie i zapytania o medianę w czasie logarytmicznym).

Wydaje mi się to jednak trochę głupie. Skutecznie nauczymy się wszystkich statystyk zamówień we wszystkich oknach wielkości k , nie tylko median. Co więcej, w praktyce nie jest to zbyt atrakcyjne, szczególnie jeśli k jest duże (duże drzewa wyszukiwania są zwykle wolne, narzut w zużyciu pamięci nie jest trywialny, wydajność pamięci podręcznej jest często niska itp.).

Czy możemy zrobić coś znacznie lepszego?

Czy są jakieś dolne granice (np. Czy trywialny algorytm jest asymptotycznie optymalny dla modelu porównawczego)?


Edycja: David Eppstein podał dolną granicę dla modelu porównawczego! Zastanawiam się, czy można zrobić coś nieco sprytniejszego niż prosty algorytm?

Na przykład, czy moglibyśmy zrobić coś wzdłuż tych linii: podzielić wektor wejściowy na części wielkości k ; posortuj każdą część (śledząc oryginalne pozycje każdego elementu); a następnie użyć wektora posortowanego w części, aby znaleźć działające mediany wydajnie bez żadnych struktur danych pomocniczych? Oczywiście nadal byłoby to O(nlogk) , ale w praktyce sortowanie tablic jest zwykle znacznie szybsze niż utrzymywanie drzew wyszukiwania.


Edycja 2: Saeed chciał zobaczyć kilka powodów, dla których myślę, że sortowanie jest szybsze niż operacje drzewa wyszukiwania. Oto bardzo szybkie testy porównawcze, dla k=107 , n=108 :

  • ≈ 8s: sortowanie n/k wektorów z k elementami każdy
  • ≈ 10s: sortowanie wektora z elementamin
  • ≈ lata 80 .: wstawiania i usuwania w tablicy mieszającej o rozmiarzenk
  • ≈ 390s: wstawiania i usuwania w zrównoważonym drzewie wyszukiwania o rozmiarzenk

Tabela skrótów jest tylko dla porównania; nie ma bezpośredniego zastosowania w tej aplikacji.

Podsumowując, mamy prawie 50-krotną różnicę w wydajności sortowania vs. zrównoważone operacje drzewa wyszukiwania. I wszystko pogorszy się, jeśli zwiększymy .k

(Szczegóły techniczne: Dane = losowe 32-bitowe liczby całkowite. Komputer = typowy nowoczesny laptop. Kod testowy został napisany w C ++, przy użyciu standardowych procedur bibliotecznych (std :: sort) i struktur danych (std :: multiset, std :: unsorted_multiset). Użyłem dwóch różnych kompilatorów C ++ (GCC i Clang) oraz dwóch różnych implementacji biblioteki standardowej (libstdc ++ i libc ++). Tradycyjnie, std :: multiset został zaimplementowany jako wysoce zoptymalizowane drzewo czerwono-czarne.)

Jukka Suomela
źródło
1
Nie sądzę, że będziesz w stanie ulepszyć . Powodem jest to, że jeśli spojrzysz na okno , nigdy nie można wykluczyć żadnej liczby od bycia medianami przyszłego okna. Oznacza to, że w każdej chwili musisz zachować przynajmniej liczby całkowite w strukturze danych, i nie wydaje się, aby aktualizowała się w czasie krótszym niż dziennik. nlogkxt,...,xt+k1xt+k2,...,xt+k1k2
RB
Twój trywialny algorytm wydaje mi się nie , czy coś źle zrozumiałem? I myślę, że z tego powodu masz problem z dużym , w przeciwnym razie czynnik logarytmiczny nie ma znaczenia w praktycznych zastosowaniach, a także nie ma dużej ukrytej stałej w tym algorytmie. O((nk)klogk)O(nlogk)k
Saeed
@Saeed: W trywialnym algorytmie elementy są przetwarzane jeden po drugim; w kroku dodajesz do drzewa wyszukiwania i (jeśli ) również usuwasz z drzewa wyszukiwania. To jest kroków, z których każdy zajmuje czas . ixii>kxiknO(logk)
Jukka Suomela
Masz na myśli zrównoważone drzewo wyszukiwania, a nie zwykłe drzewo wyszukiwania?
Saeed
1
@ Saeed: Pamiętaj, że w moich testach porównawczych nawet nie próbowałem znaleźć median. Właśnie zrobiłem wstawień i usunięć w drzewie wyszukiwania o rozmiarze , a operacje te z pewnością zajmą czas . Musisz tylko zaakceptować fakt, że operacje drzewa wyszukiwania są w praktyce bardzo wolne, w porównaniu z sortowaniem. Zobaczysz to łatwo, jeśli spróbujesz napisać algorytm sortowania, który działa poprzez dodanie elementów do zrównoważonego drzewa wyszukiwania - z pewnością działa w czasie , ale w praktyce będzie absurdalnie powolny, a także zmarnuje dużo pamięci. nnkO(logk)O(nlogn)
Jukka Suomela

Odpowiedzi:

32

Oto dolna granica sortowania. Biorąc pod uwagę zestaw wejściowy o długości do posortowania, utwórz dane wejściowe do swojego problemu z medianą, składającego się z kopii liczby mniejszej niż minimum , następnie samego , a następnie kopii liczby większej niż maksimum i ustawić . Te prowadzące mediany tego wejścia są takie same jak na posortowanych w .Snn1SSn1Sk=2n1S

Zatem w porównawczym modelu obliczeniowym wymagany jest czas . Być może, jeśli dane wejściowe są liczbami całkowitymi i używasz algorytmów sortowania liczb całkowitych, możesz to zrobić lepiej.Ω(nlogn)

David Eppstein
źródło
6
Ta odpowiedź naprawdę sprawia, że ​​zastanawiam się, czy konwersacja również się utrzymuje: czy mając wydajny algorytm sortowania, czy otrzymujemy wydajny działający algorytm mediany? (Na przykład, czy efektywny algorytm sortowania liczb całkowitych implikuje wydajny działający algorytm medianowy dla liczb całkowitych? Czy też algorytm sortowania efektywny IO zapewnia działający algorytm medianowy?)
Jukka Suomela
1
Jeszcze raz wielkie dzięki za odpowiedź, to naprawdę ustawiło mnie na właściwej drodze i dało inspirację dla opartego na sortowaniu algorytmu filtru medianowego! W końcu udało mi się znaleźć artykuł z 1991 r., Który przedstawił zasadniczo ten sam argument, co tu podasz, a Pat Morin wskazał na inny odpowiedni artykuł z 2005 r .; patrz referencje. [6] i [9] tutaj .
Jukka Suomela,
9

Edycja: ten algorytm jest teraz prezentowany tutaj: http://arxiv.org/abs/1406.1717


Tak, aby rozwiązać ten problem, wystarczy wykonać następujące operacje:

  • Sortuj wektorów, każdy z elementami.n/kk
  • Wykonaj przetwarzanie końcowe w czasie liniowym.

Z grubsza chodzi o to, że:

  • Rozważ dwa sąsiednie bloki wejściowe, i b , oba z elementami k ; pozwolić elementów być 1 , 2 , . . . , K i b 1 , b 2 , . . . , b k w kolejności pojawienia się w wektorze wejściowym x .abka1,a2,...,akb1,b2,...,bkx
  • Posortuj te bloki i poznaj pozycję każdego elementu w bloku.
  • Rozszerz wektory i b za pomocą wskaźników poprzedników / następców, aby postępując zgodnie z łańcuchami wskaźników, można było przemieszczać elementy w kolejności rosnącej. W ten sposób stworzyliśmy podwójnie połączone listy a i b .abab
  • Jeden po drugim, usunąć wszystkie elementy z listy połączonej , w odwrotnej kolejności występowania b k , b k - 1 , . . . , b 1 . Ilekroć usuwamy element, pamiętaj, jaki był jego następca i poprzednik w momencie usunięcia .bbk,bk1,...,b1
  • Teraz utrzymać „średnie wskaźniki” i q , które wskazują na listach ' i b ' , odpowiednio. Uruchomienie P do punktu środkowego ' i inicjalizacji Q do ogona pustego liście b ' .pqabpaqb
  • Dla każdego :i

    • Usuń z listy A ' (jest to O ( 1 ) czas, po prostu usunąć z połączonej listy). Porównać do I z elementem wskazanych przez p aby zobaczyć, czy usunięte przed lub po p .aiaO(1)aipp
    • Umieścić powrót do listy B ' w swojej pierwotnej pozycji (jest to O ( 1 ) czas, my zapamiętane poprzednik i następca b I ). Porównaj b I z elementem wskazanym przez q , aby zobaczyć, czy dodaliśmy element przed lub po q .bibO(1)bibiqq
    • Zaktualizuj wskaźniki i q, tak aby mediana połączonej listy a b znajdowała się w punkcie p lub w punkcie q . (Jest to czas O ( 1 ) , wystarczy postępować zgodnie z listami połączonymi jeden lub dwa kroki, aby wszystko naprawić. Będziemy śledzić, ile elementów znajduje się przed / po p i q na każdej liście, i zachowamy niezmienność, że oba p i q wskazują elementy, które są jak najbliżej mediany).pqabpqO(1)pqpq

Połączone listy są tylko tablicami indeksów typu element, więc są lekkie (poza tym, że lokalizacja dostępu do pamięci jest niska).k


Oto przykładowa implementacja i testy porównawcze:

Oto wykres czasów działania (dla ):n2106

  • Niebieski = sortowanie + przetwarzanie końcowe, .O(nlogk)
  • Zielony = utrzymuj dwa stosy, , wdrożenie z https://github.com/craffel/median-filterO(nlogk)
  • Czerwony = utrzymuj dwa drzewa wyszukiwania, .O(nlogk)
  • Czarny = zachowaj posortowany wektor, .O(nk)
  • Oś X = rozmiar okna ( ).k/2)
  • Oś Y = czas pracy w sekundach.
  • Dane = 32-bitowe liczby całkowite i losowe 64-bitowe liczby całkowite, z różnych dystrybucji.

czasy pracy

Jukka Suomela
źródło
3

Biorąc pod uwagę granice Davida, jest mało prawdopodobne, abyś mógł zrobić lepszy najgorszy przypadek, ale istnieją lepsze algorytmy wrażliwe na wyniki. W szczególności, jeśli wyniku jest liczba median, możemy rozwiązać problem w czasie O ( n log m + m log n ) .mO(nlogm+mlogn)

Aby to zrobić, zastąp zbalansowane drzewo binarne zrównoważonym drzewem binarnym składającym się tylko z tych elementów, które w przeszłości były medianami, plus dwa stosy Fibonacciego między każdą parą poprzednich median (po jednym dla każdego kierunku), a także zliczaj, abyśmy mogli zlokalizuj, która kupa Fibonacciego zawiera określony element w kolejności. Nie zawracaj sobie głowy usuwaniem elementów. Po wstawieniu nowego elementu możemy zaktualizować naszą strukturę danych w czasie . Jeśli nowe liczby wskazują, że mediana znajduje się w jednym z hałd Fibonacciego, potrzeba dodatkowego O ( log n ), aby wyciągnąć nową medianę. To O ( log n )O(logm)O(logn)O(logn) ładunek występuje tylko raz na medianę.

Gdyby istniał czysty sposób usuwania elementów bez uszkadzania ładnej złożoności Fibonacciego, przechodzilibyśmy do , ale nie jestem pewien, czy jest to możliwe.O(nlogm+mlogk)

Geoffrey Irving
źródło
Ups, to nie działa tak, jak napisano, ponieważ jeśli nie usuniesz elementów, liczby nie odzwierciedlą nowego okna. Nie jestem pewien, czy można to naprawić, ale zostawię odpowiedź na wypadek, gdyby był sposób.
Geoffrey Irving
Myślę więc, że ten algorytm może faktycznie przyjąć jeśli usuniesz węzły z hałd Fibonacciego, ponieważ głębokość sterty Fibonacciego wzrasta tylko wtedy, gdy wywoływane jest delete-min. Czy ktoś zna ładne granice złożoności sterty Fibonacciego, biorąc pod uwagę liczbę wywołań delete-min? O(nlogm)
Geoffrey Irving
uwaga dodatkowa: pytanie nie jest jasne, podstawowa struktura danych nie jest zdefiniowana, po prostu wiemy coś bardzo niejasnego. jak chcesz poprawić coś, czego nie wiesz, co to jest? jak chcesz porównać swoje podejście?
Saeed
1
Przepraszam za niepełną pracę. Poprosiłem konkretne pytanie potrzebne, aby rozwiązać tę odpowiedź tutaj: cstheory.stackexchange.com/questions/21778/... . Jeśli uważasz, że to właściwe, mogę usunąć tę odpowiedź, dopóki drugie pytanie nie zostanie rozwiązane.
Geoffrey Irving