Mam tablicę 100 000 ciągów o długości . Chcę porównać każdy ciąg z każdym innym, aby zobaczyć, czy dwa ciągi różnią się o 1 znak. W tej chwili, gdy dodam każdy ciąg do tablicy, sprawdzam go względem każdego łańcucha już w tablicy, który ma złożoność czasową .
Czy istnieje struktura danych lub algorytm, który może porównywać ciągi ze sobą szybciej niż to, co już robię?
Niektóre dodatkowe informacje:
Sprawy porządku:
abcde
axbcde
różnią się o 1 znak, podczasabcde
iedcba
różnią się o 4 znaków.Dla każdej pary ciągów, które różnią się jednym znakiem, usunę jeden z tych ciągów z tablicy.
W tej chwili szukam ciągów, które różnią się tylko o 1 znak, ale byłoby miło, gdyby różnicę o 1 znak można było zwiększyć, powiedzmy, o 2, 3 lub 4 znaki. Jednak w tym przypadku uważam, że wydajność jest ważniejsza niż zdolność do zwiększenia limitu różnic postaci.
jest zwykle w zakresie 20–40.
Odpowiedzi:
Możliwe jest osiągnięcie czasu najgorszego przypadku.O(nklogk)
Zacznijmy prosto. Jeśli zależy Ci na łatwym do wdrożenia rozwiązaniu, które będzie wydajne na wielu nakładach, ale nie na wszystkich, oto proste, pragmatyczne, łatwe do wdrożenia rozwiązanie, które w praktyce wystarcza w wielu sytuacjach. W najgorszym przypadku jednak wraca do kwadratu.
Weź każdy ciąg i przechowuj go w tablicy mieszającej, wpisanej w pierwszej połowie łańcucha. Następnie iteruj po kubełkach z mieszaniem. Dla każdej pary ciągów w tym samym wiadrze sprawdź, czy różnią się one 1 znakiem (tj. Sprawdź, czy ich druga połowa różni się 1 znakiem).
Następnie weź każdy ciąg i zapisz go w tablicy mieszającej, tym razem wpisanej w drugiej połowie łańcucha. Ponownie sprawdź każdą parę ciągów w tym samym wiadrze.
Zakładając, że łańcuchy są dobrze rozłożone, czas działania będzie prawdopodobnie wynosił około . Ponadto, jeśli istnieje para ciągów, które różnią się o 1 znak, zostanie znaleziona podczas jednego z dwóch przebiegów (ponieważ różnią się tylko 1 znakiem, ten różniący się znak musi znajdować się w pierwszej lub drugiej połowie ciągu, więc druga lub pierwsza połowa łańcucha musi być taka sama). Jednak w najgorszym przypadku (np. Jeśli wszystkie ciągi znaków zaczynają się lub kończą tymi samymi znakami k / 2 ), zmniejsza się to do czasu pracy O ( n 2 k ) , więc jego czas działania w najgorszym przypadku nie jest poprawą brutalnej siły .O(nk) k/2 O(n2k)
W celu optymalizacji wydajności, jeśli jakikolwiek segment zawiera zbyt wiele łańcuchów, możesz powtórzyć ten sam proces rekurencyjnie, aby wyszukać parę, która różni się jednym znakiem. Wywołanie rekurencyjne będzie na ciągach o długości .k/2
Jeśli zależy Ci na najgorszym czasie działania:
Przy powyższej optymalizacji wydajności uważam, że najgorszym czasem działania jest .O(nklogk)
źródło
Moje rozwiązanie jest podobne do j_random_hackera, ale używa tylko jednego zestawu skrótów.
Stworzyłbym zestaw skrótów ciągów. Dla każdego ciągu wejściowego dodaj do zestawu ciągów. W każdym z tych ciągów zastąp jedną z liter znakiem specjalnym, którego nie ma w żadnym z nich. Podczas ich dodawania sprawdź, czy nie ma ich jeszcze w zestawie. Jeśli tak, to masz dwa ciągi, które różnią się (najwyżej) jednym znakiem.k
Przykład z ciągami „abc”, „adc”
W przypadku abc dodajemy „* bc”, „a * c” i „ab *”
W przypadku adc dodajemy „* dc”, „a * c” i „ad *”
Kiedy dodamy „a * c” za drugim razem, zauważymy, że jest już w zestawie, więc wiemy, że istnieją dwa ciągi, które różnią się tylko jedną literą.
Całkowity czas działania tego algorytmu wynosi . Jest tak, ponieważ tworzymy k nowych ciągów dla wszystkich n ciągów na wejściu. Dla każdego z tych ciągów musimy obliczyć skrót, co zwykle zajmuje czas O ( k ) .O(n∗k2) k n O(k)
Przechowywanie wszystkich ciągów zajmuje przestrzeń .O(n∗k2)
Dalsze doskonalenia
Możemy jeszcze bardziej ulepszyć algorytm, nie przechowując bezpośrednio zmodyfikowanych ciągów, ale zamiast tego przechowując obiekt z odniesieniem do oryginalnego ciągu i indeksu zamaskowanego znaku. W ten sposób nie musimy tworzyć wszystkich ciągów i potrzebujemy tylko miejsca do przechowywania wszystkich obiektów.O(n∗k)
Będziesz musiał zaimplementować niestandardową funkcję skrótu dla obiektów. Możemy wziąć implementację Java jako przykład, zobacz dokumentację Java . Java hashCode zwielokrotnia wartość Unicode każdego znaku przez (przy k długości łańcucha i i indeksie jednego znaku. Zauważ, że każdy zmieniony łańcuch różni się tylko o jeden znak od oryginału. Możemy łatwo obliczyć wkład tego znaku w kod skrótu. Możemy go odjąć i dodać nasz znak maskowania. To wymaga O ( 1 ) do obliczenia. To pozwala nam obniżyć całkowity czas działania do O ( n31k−i k i O(1) O(n∗k)
źródło
equals
ihashCode
metodami, które mogą działać. Samo utworzenie łańcucha w stylu a * b w tych metodach powinno uczynić go kuloodpornym; Podejrzewam, że niektóre inne odpowiedzi tutaj będą miały problemy z kolizją skrótu.*bc
,a*c
,ab*
. Zastanawiam się, czy można to pokazać jako niemożliwe?Zrobiłbym tablic H 1 , … , H k , z których każdy ma ciąg ( k - 1 ) jako klucz i listę liczb (identyfikatorów ciągów) jako wartość. Tablica skrótów H i będzie zawierać wszystkie przetworzone do tej pory ciągi znaków, ale ze znakiem w pozycji i zostanie usunięty . Na przykład, jeśli k = 6 , wówczas H 3 [ A B D E F ] będzie zawierać listę wszystkich dotychczas widzianych łańcuchów, które mają wzór Ak H1,…,Hk (k−1) Hi i k=6 H3[ABDEF] , gdzie ⋅ oznacza „dowolny znak”. Następnie przetworzyć j -tej ciąg wejściowy s j :AB⋅DEF ⋅ j sj
Jeśli przechowujemy każdy klucz skrótu jawnie, musimy użyć przestrzeni i tym samym mieć co najmniej złożoność czasową. Ale jak opisano przez Simona Prinsa , możliwe jest reprezentowanie serii modyfikacji łańcucha (w jego przypadku opisanego jako zamiana pojedynczych znaków na , w moich jako usunięcie) niejawnie w taki sposób, że wszystkie k kluczy skrótu dla określonego łańcucha muszą tylko O ( k ) spacja, prowadząca do ogólnej przestrzeni O ( n k ) i otwierająca możliwość O ( n k )O(nk2) k O(k) O(nk) O(nk) czas też. Aby osiągnąć tę złożoność czasową, potrzebujemy sposobu obliczenia skrótów dla wszystkich wariantów długości ciągu- k w czasie O ( k ) : na przykład można to zrobić za pomocą skrótów wielomianowych, jak sugeruje DW (i to jest prawdopodobnie znacznie lepiej niż po prostu XORing usuniętego znaku za pomocą skrótu dla oryginalnego łańcucha).k k O(k)
*
Sztuczka niejawnej reprezentacji Simona Prinsa oznacza również, że „usunięcie” każdego znaku nie jest faktycznie wykonywane, więc możemy użyć zwykłej reprezentacji ciągu opartej na tablicy bez ograniczenia wydajności (zamiast połączonych list, jak pierwotnie sugerowałem).
źródło
Oto bardziej niezawodne podejście hashujące niż metoda wielomianowa. Najpierw wygenerować losowymi liczbami całkowitymi dodatnimi r 1 .. k , które są względnie pierwsze z hashtable rozmiarze M . Mianowicie, 0 ≤ r i < M . Następnie mieszania każdy łańcuch x 1 .. k na ( Σ k i = 1 X I r I ) mod M . Prawie nic nie może zrobić przeciwnik, aby spowodować bardzo nierównomierne kolizje, ponieważ generujesz r 1 .. kw czasie wykonywania i tak jak kk r1..k M 0≤ri<M x1..k (∑ki=1xiri)modM r1..k k zwiększa maksymalne prawdopodobieństwo kolizji danych dwóch różnych łańcuchów szybko przechodzi do . Oczywiste jest również, jak obliczyć w czasie O ( k ) wszystkie możliwe wartości skrótu dla każdego łańcucha ze zmienionym jednym znakiem.1/M O(k)
Jeśli naprawdę chcesz zagwarantować jednolite haszowanie, możesz wygenerować jedną losową liczbę naturalną mniejszą niż M dla każdej pary ( i , c ) dla i od 1 do k i dla każdego znaku c , a następnie haszować każdy ciąg x 1 .. k do ( ∑ k i = 1 r ( i , x i ) ) mod Mr(i,c) M (i,c) i 1 k c x1..k (∑ki=1r(i,xi))modM . Wtedy prawdopodobieństwo kolizji z każdej pary różnych ciągów jest dokładnie . To podejście jest lepsze, jeśli twój zestaw znaków jest stosunkowo mały w porównaniu do n .1/M n
źródło
Wiele opublikowanych tutaj algorytmów zajmuje sporo miejsca na tablicach skrótów. Oto prosty algorytm pamięci dyskowej O ( ( n lg n ) ⋅ k 2 ) .O(1) O((nlgn)⋅k2)
Sztuką jest użycie , który jest komparatorem między dwiema wartościami a i b, która zwraca wartość true, jeśli a < b (leksykograficznie) zignoruje k- ty znak. Następnie algorytm jest następujący.Ck(a,b) a b a<b k
Po pierwsze, po prostu sortuj ciągi regularnie i wykonaj skanowanie liniowe, aby usunąć duplikaty.
Następnie dla każdego :k
Posortuj ciągi znaków za pomocą jako komparatora.Ck
Ciągi, które różnią się tylko są teraz sąsiadujące i można je wykryć w skanie liniowym.k
źródło
Dwa ciągi długości k , różniące się jednym znakiem, dzielą prefiks długości l i sufiks długości m taki, że k = l + m + 1 .
Odpowiedź Simona Prinsa koduje to, przechowując wszystkie kombinacje prefiksów / sufiksów jawnie, tzn.
abc
Staje się*bc
,a*c
iab*
. To k = 3, l = 0,1,2 i m = 2,1,0.Jak wskazuje valarMorghulis, możesz organizować słowa w drzewie prefiksów. Istnieje również bardzo podobne drzewo sufiksów. Dość łatwo jest rozszerzyć drzewo o liczbę węzłów liści poniżej każdego przedrostka lub przyrostka; można to zaktualizować w O (k) podczas wstawiania nowego słowa.
Powodem, dla którego chcesz, aby liczba rodzeństwa była liczona, jest to, aby wiedzieć, biorąc pod uwagę nowe słowo, czy chcesz wyliczyć wszystkie ciągi z tym samym przedrostkiem, czy też wyliczyć wszystkie ciągi z tym samym przyrostkiem. Np. Dla „abc” jako danych wejściowych możliwe prefiksy to „”, „a” i „ab”, podczas gdy odpowiednie sufiksy to „bc”, „c” i „”. Jak widać, w przypadku krótkich sufiksów lepiej wyliczyć rodzeństwo w drzewie prefiksów i odwrotnie.
Jak wskazuje @einpoklum, z pewnością możliwe jest, że wszystkie ciągi mają ten sam przedrostek k / 2 . To nie jest problem w tym podejściu; drzewo prefiksów będzie liniowe do głębokości k / 2, a każdy węzeł do głębokości k / 2 będzie przodkiem 100 000 węzłów liści. W rezultacie drzewo sufiksów będzie używane do głębokości (k / 2-1), co jest dobre, ponieważ ciągi znaków muszą różnić się sufiksami, ponieważ mają wspólne prefiksy.
[edytuj] Jako optymalizacja, po określeniu najkrótszego unikalnego prefiksu ciągu, wiesz, że jeśli istnieje jeden inny znak, musi to być ostatni znak prefiksu, a po znalezieniu prawie duplikatu sprawdzanie prefiksu, który był o jeden krótszy. Jeśli więc „abcde” ma najkrótszy unikalny przedrostek „abc”, oznacza to, że istnieją inne ciągi zaczynające się od „ab?” ale nie z „abc”. Tj. Gdyby różniły się tylko jedną postacią, byłaby to trzecia postać. Nie musisz już sprawdzać „abc? E”.
Zgodnie z tą samą logiką, jeśli okaże się, że „cde” jest unikalnym najkrótszym sufiksem, to wiesz, że musisz sprawdzić tylko przedrostek o długości 2 „ab”, a nie przedrostek o długości 1 lub 3.
Zauważ, że ta metoda działa tylko dla dokładnie jednej różnicy między znakami i nie uogólnia do 2 różnic między znakami, polega na tym, że jeden jeden znak jest oddzieleniem identycznych przedrostków i identycznych przyrostków.
źródło
Przechowywanie ciągów w wiadrach jest dobrym sposobem (istnieją już różne odpowiedzi na ten temat).
Alternatywnym rozwiązaniem może być przechowywanie ciągów na posortowanej liście. Sztuką jest sortowanie według algorytmu mieszającego uwzględniającego lokalizację . Jest to algorytm mieszający, który daje podobne wyniki, gdy dane wejściowe są podobne [1].
Jednym z możliwych algorytmów mieszających wrażliwych na lokalizację może być Nilsimsa (z implementacją open source dostępną na przykład w Pythonie ).
[1]: Zauważ, że często algorytmy mieszające, takie jak SHA1, są zaprojektowane w odwrotny sposób: wytwarzają bardzo różne skróty dla podobnych, ale nie równych danych wejściowych.
Uwaga: Szczerze mówiąc, osobiście zaimplementowałbym jedno z zagnieżdżonych / zorganizowanych pod kątem drzew rozwiązań dla aplikacji produkcyjnych. Jednak pomysł posortowanej listy wydał mi się interesującą alternatywą. Zauważ, że ten algorytm w dużym stopniu zależy od wybranego algorytmu skrótu. Nilsimsa to jeden algorytm, który znalazłem - istnieje jednak wiele innych (na przykład TLSH, Ssdeep i Sdhash). Nie sprawdziłem, czy Nilsimsa działa z moim zarysowanym algorytmem.
źródło
Możesz użyć biblioteki SDSL do zbudowania tablicy sufiksów w skompresowanej formie i odpowiedzi na zapytania LCP.
źródło
k
*
*bcde
a*cde
Możesz także użyć tego podejścia, aby podzielić pracę na wiele rdzeni CPU / GPU.
źródło
To jest krótka wersja odpowiedzi @ SimonPrins, która nie zawiera skrótów.
Zakładając, że żaden z łańcuchów nie zawiera gwiazdki:
Alternatywne rozwiązanie z niejawnym użyciem skrótów w Pythonie (nie może się oprzeć pięknu):
źródło
Oto moje zdanie na temat wyszukiwarki niezgodności 2+. Zauważ, że w tym poście uważam każdy łańcuch za okrągły, np. Podciąg o długości 2 przy indeksie
k-1
składa się z symbolu,str[k-1]
po którym następujestr[0]
. A podciąg o długości 2 przy indeksie-1
jest taki sam!M
k
M
k=20
M=4
abcd*efgh*ijkl*mnop*
Teraz algorytm wyszukiwania wszystkich niedopasowań do
M
symboli wśród ciągówk
symboli:str[i..i+L-1]
, gdzieL = mlen(k,M)
. JeśliL=4
masz alfabet składający się tylko z 4 symboli (z DNA), utworzy to 256 grup.L
już dopasowanych symbolach grupystr[i..i+L1-1]
, gdzieL1 = mlen(k-L,M)
. Fe, jeślik=20, M=4, alphabet of 4 symbols
takL=4
iL1=3
to, utworzy 64 grupy.Dlaczego nie zaczynamy
j
od 0? Ponieważ już utworzyliśmy te grupy z tą samą wartościąi
, więc zadanie zj<=i-L
będzie dokładnie równoważne zadaniu z zamienionymi wartościami i i.Dalsze optymalizacje:
str[i..i+L-2] & str[i+L]
. To tylko podwaja liczbę utworzonych miejsc pracy, ale pozwala na zwiększenieL
o 1 (jeśli moja matematyka jest poprawna). Tak więc fe zamiast 256 grup podzielisz dane na 1024 grupy.*
0..k-1
M-1
k-1
źródło
Codziennie pracuję nad wynalezieniem i optymalizacją alg, więc jeśli potrzebujesz ostatniej wydajności, oto plan:
*
każdą pozycją niezależnie, tj. Zamiastn*k
wariantów ciągów przetwarzania pojedynczego zadania - uruchamiajk
niezależne zadania dla każdego sprawdzanian
łańcucha. Możesz rozłożyć tek
zadania na wiele rdzeni CPU / GPU. Jest to szczególnie ważne, jeśli chcesz sprawdzić różnice między znakami 2+. Mniejszy rozmiar zadania poprawi również lokalizację pamięci podręcznej, co samo w sobie może przyspieszyć program 10 razy.*
i-tą pozycją) i indeks łańcucha, a następnie albo posortuj je, albo utwórz tabelę skrótów z tych rekordów.Do sortowania możesz wypróbować następującą kombinację:
źródło