Wydajne algorytmy dla problemu widoczności pionowej

18

Myśląc o jednym problemie, zdałem sobie sprawę, że muszę stworzyć wydajny algorytm rozwiązujący następujące zadanie:

Problem: otrzymujemy dwuwymiarowe kwadratowe pudełko o boku n którego boki są równoległe do osi. Możemy spojrzeć na to z góry. Istnieją jednak również m segmenty poziome. Każdy segment ma całkowitą współrzędną y ( 0yn ) i współrzędne x ( 0x1<x2n ) i łączy punkty (x1,y) i (x2,y) (spójrz na zdjęcie poniżej).

Chcielibyśmy wiedzieć, dla każdego segmentu jednostki na górze pudełka, jak głęboko możemy spojrzeć w środku w pionie, jeśli przejrzymy ten segment.

x{0,,n1}maxi: [x,x+1][x1,i,x2,i]yi

Przykład: biorąc pod uwagę i segmentów zlokalizowanych jak na poniższym obrazku, wynikiem jest . Zobacz, jak głębokie światło może dostać się do pudełka.m = 7 ( 5 , 5 , 5 , 3 , 8 , 3 , 7 , 8 , 7 )n=9m=7(5,5,5,3,8,3,7,8,7)

Siedem segmentów;  część zacieniowana wskazuje obszar, do którego można dotrzeć światłem

Na szczęście dla nas zarówno , jak i mdość małe i możemy wykonywać obliczenia offline.mnm

Najłatwiejszym algorytmem rozwiązującym ten problem jest brute-force: dla każdego segmentu przejdź przez całą tablicę i zaktualizuj ją w razie potrzeby. Daje nam jednak niezbyt imponujące .O(mn)

Dużym ulepszeniem jest użycie drzewa segmentów, które jest w stanie zmaksymalizować wartości segmentu podczas zapytania i odczytać wartości końcowe. Nie będę tego dalej opisywał, ale widzimy, że złożoność czasu wynosi .O((m+n)logn)

Wymyśliłem jednak szybszy algorytm:

Zarys:

  1. Posortuj segmenty w malejącej kolejności współrzędnej (czas liniowy przy użyciu odmiany sortowania zliczającego). Teraz pamiętać, że jeśli szerokości jednostek, niskoprofilowy segment został objęty żadnym segmencie przed, nie po segment może związany wiązki światła przechodzącego przez ten szerokości jednostek, niskoprofilowy segmencie już. Następnie zrobimy przeciągnięcie linii od góry do dołu pudełka.x xyxx

  2. Teraz wprowadzimy kilka definicji: -jednostka segmentowa jest urojonym poziomym segmentem na przemiataniu, którego współrzędnymi są liczby całkowite i których długość wynosi 1. Każdy segment podczas procesu zamiatania może być albo nieoznaczony (to znaczy wiązka światła wychodząca z góra pudełka może dosięgnąć tego segmentu) lub oznaczona (odwrotna wielkość liter). Rozważ segment unit z , zawsze niezaznaczone. również zestawy . Każdy zestaw będzie zawierał całą sekwencję następujących po sobie segmentów jednostek (jeśli istnieją) z następującymi nieoznaczonymix x x 1 = n x 2 = n + 1 S 0 = { 0 } , S 1 = { 1 } , , S n = { n } xxxxx1=nx2=n+1S0={0},S1={1},,Sn={n} x człon.

  3. Potrzebujemy struktury danych, która byłaby w stanie działać skutecznie na tych segmentach i zestawach. Użyjemy struktury find-union rozszerzonej o pole zawierające maksymalny indeks segmentu -jednostek (indeks segmentu nieoznaczonego ).x

  4. Teraz możemy efektywnie obsługiwać segmenty. Powiedzmy, że rozważamy teraz ty segment w kolejności (nazwij go „zapytaniem”), który zaczyna się od a kończy na . Musimy znaleźć wszystkie nieoznakowane segmenty unit, które są zawarte w segmencie (są to dokładnie te segmenty, na których wiązka światła kończy swoją drogę). Wykonamy następujące czynności: po pierwsze, znajdziemy pierwszy nieoznaczony segment w zapytaniu ( Znajdź reprezentanta zestawu, w którym znajduje się i uzyskaj maksymalny indeks tego zestawu, który z definicji jest segmentem nieoznaczonym ). Następnie ten indeksx 1 x 2 x i x 1 x y x x + 1 x x 2ix1x2 xix1xznajduje się w zapytaniu, dodaj go do wyniku (wynikiem dla tego segmentu jest ) i zaznacz ten indeks ( zestawy Unii zawierające i ). Następnie powtarzaj tę procedurę, aż znajdziemy wszystkie nieoznakowane segmenty, czyli następne zapytanie Find daje nam indeks .yxx+1xx2

Zauważ, że każda operacja szukania unii zostanie wykonana tylko w dwóch przypadkach: albo zaczniemy rozważać segment (co może się zdarzyć razy), albo właśnie oznaczyliśmy segment jednostki (może się to zdarzyć razy). Zatem ogólna złożoność wynosi ( jest odwrotną funkcją Ackermanna ). Jeśli coś nie jest jasne, mogę rozwinąć więcej na ten temat. Może będę mógł dodać trochę zdjęć, jeśli będę miał trochę czasu.mxnO((n+m)α(n))α

Teraz dotarłem do „ściany”. Nie mogę wymyślić algorytmu liniowego, choć wydaje się, że powinien istnieć jeden. Mam więc dwa pytania:

  • Czy istnieje algorytm czasu liniowego (to znaczy ) rozwiązujący problem widoczności segmentu poziomego?O(n+m)
  • Jeśli nie, to jaki jest dowód na to, że problemem z widocznością jest ?ω(n+m)
mnbvmar
źródło
Jak szybko sortujesz swoje m segmenty?
babou
@babou, pytanie określa sortowanie zliczające, które, jak mówi pytanie, biegnie w czasie liniowym („czas liniowy z zastosowaniem wariantu sortowania zliczającego”).
DW
Czy próbowałeś zamiatać od lewej do prawej? Wystarczy posortować na i x 2 zarówno w krokach O ( m ), jak i O ( m ), aby przejść w prawo. Tak więc w sumie O ( m ) . x1x2O(m)O(m)O(m)
invalid_id
@invalid_id Tak, próbowałem. Jednak w tym przypadku linia przeciągnięcia musi odpowiednio zareagować, gdy spotka się z początkiem odcinka (innymi słowy, dodaj liczbę równą współrzędnej segmentu do multisetu), dotrze do końca odcinka (usuń występowanie y -coordinate) i wyprowadza najwyższy aktywny segment (maksymalna wartość wyjściowa w multiset). Nie słyszałem o żadnych strukturach danych pozwalających nam to robić w (zamortyzowanym) stałym czasie. yy
mnbvmar
@mnbvmar może głupia sugestia, ale co z tablicą o rozmiarze , zamiatasz i zatrzymujesz każdą komórkę O ( n ) . Dla każdej komórki znasz max y i możesz wprowadzić ją do macierzy, a ponadto możesz śledzić ogólne maksimum za pomocą zmiennej. nO(n)y
invalid_id

Odpowiedzi:

1
  1. Sortuj zarówno oraz x 2 współrzędne linii w dwóch oddzielnych macierzy A i B . O ( m )x1x2ABO(m)
  2. Utrzymujemy również pomocniczy rozmiar tablicy bitów aby śledzić aktywne segmenty.n
  3. Zacznij zamiatać od lewej do prawej:
  4. dla (i=0,i<n,i++)
  5. {
  6. ..if w połączeniu z y wartość c O ( 1 )x1=iyc O(1)
  7. .. {
  8. .... znajdź ( )max
  9. .... sklep ( ) O ( 1 )maxO(1)
  10. ..}
  11. ..if w połączeniu z y wartość c O ( 1 )x2=iyc O(1)
  12. .. {
  13. .... znajdź ( )max
  14. .... sklep ( ) O ( 1 )maxO(1)
  15. ..}
  16. }

find ( ) może być zaimplementowany przy użyciu tablicy bitów z n bitami. Teraz za każdym razem, gdy usuwamy lub dodajemy element do L , możemy zaktualizować tę liczbę całkowitą, ustawiając odpowiednio wartość true lub false. Teraz masz dwie opcje w zależności od użytego języka programowania, a założenie, że n jest względnie małe, tj. Mniejsze niż l o n g l o n g i n t, czyli co najmniej 64 bity lub stała liczba tych liczb całkowitych:maxnLnlonglongint

  • Uzyskaj najmniej znaczący bit w stałym czasie jest obsługiwany przez niektóre urządzenia i gcc.
  • Konwertując na liczbę całkowitą O ( 1 ) , otrzymasz maksimum (nie bezpośrednio, ale możesz je uzyskać).LO(1)

Wiem, że to dość hack, ponieważ zakłada maksymalną wartość dla a zatem n może być postrzegane jako stała wtedy ...nn

nieważny numer identyfikacyjny
źródło
Jak widzę, zakładając, że masz 64-bitowy procesor x86, jesteś w stanie obsłużyć tylko . Co jeśli n jest rzędu milionów? n64n
mnbvmar
Wtedy będziesz potrzebować więcej liczb całkowitych. Dzięki dwóm liczbom całkowitym możesz obsłużyć od do 128 itd. Zatem maksymalny krok znajdowania O ( m ) jest ukryty w wymaganej liczbie liczb całkowitych, którą nadal możesz zoptymalizować, jeśli m jest małe. Wspomniałeś w swoim pytaniu, że n jest względnie małe, więc przypuszczam, że nie jest rzędu milionów. Nawiasem mówiąc, długie long int zawsze z definicji wynosi co najmniej 64 bity, nawet na procesorze 32-bitowym. nO(m)mn
invalid_id
Oczywiście to prawda, standard C ++ definiuje long long intjako co najmniej 64-bitową liczbę całkowitą. Jednak czy nie jest tak, że jeśli jest duże, a rozmiar słowa oznaczamy jako w (zwykle w = 64 ), to każde przyjmie O ( nnww=64findczas? Wtedy otrzymalibyśmy całkowiteO(mnO(nw). O(mnw)
mnbvmar
Tak, niestety w przypadku dużych wartości tak jest. Więc teraz zastanawiam się, jak duże n będzie w Twoim przypadku i czy jest ona ograniczona. Jeśli rzeczywiście jest rzędu milionów, hacking dookoła nie będzie już działał, ale jeśli c w n dla niskich wartości c będzie szybki i praktycznie O ( n + m ) . Zatem najlepszy wybór algorytmu zależy, jak zwykle, od danych wejściowych. Na przykład dla n 100 sortowanie jest zwykle szybsze niż sortowanie scalone, nawet przy czasie działania O ( n 2 ) w porównaniu donncwncO(n+m)n100O(n2) . O(nlogn)
invalid_id
3
Jestem zdezorientowany twoim wyborem formatowania. Wiesz, że możesz tutaj wpisać kod, prawda?
Raphael
0

Nie mam algorytmu liniowego, ale ten wydaje się być O (m log m).

Sortuj segmenty na podstawie pierwszej współrzędnej i wysokości. Oznacza to, że (x1, l1) zawsze występuje przed (x2, l2) zawsze, gdy x1 <x2. Również (x1, l1) na wysokości y1 występuje przed (x1, l2) na wysokości y2 za każdym razem, gdy y1> y2.

Dla każdego podzestawu o tej samej pierwszej współrzędnej wykonujemy następujące czynności. Niech pierwszy segment będzie (x1, L). Dla wszystkich pozostałych segmentów podzbioru: Jeśli segment jest dłuższy niż pierwszy, zmień go z (x1, xt) na (L, xt) i dodaj go do podzestawu L w odpowiedniej kolejności. W przeciwnym razie upuść. Wreszcie, jeśli następny podzbiór ma pierwszą współrzędną mniejszą niż L, wówczas podziel (x1, L) na (x1, x2) i (x2, L). Dodaj (x2, L) do następnego podzbioru we właściwej kolejności. Możemy to zrobić, ponieważ pierwszy segment w podzbiorze jest wyższy i obejmuje zakres od (x1, L). Ten nowy segment może być tym, który obejmuje (L, x2), ale nie będziemy tego wiedzieć, dopóki nie spojrzymy na podzbiór, który ma pierwszą współrzędną L.

Po przejściu przez wszystkie podzbiory będziemy mieli zestaw segmentów, które się nie nakładają. Aby ustalić, jaka jest wartość Y dla danego X, musimy tylko przejść przez pozostałe segmenty.

Na czym więc polega złożoność: Sortowanie to O (m log m). Pętla przez podzbiory to O (m). Wyszukiwanie to także O (m).

Wydaje się więc, że ten algorytm jest niezależny od n.

Nikt w szczególności
źródło