Projektowanie i analiza algorytmów związanych z tekstami i ciągami. Zasadniczym problemem jest zrozumienie struktury wielu skomplikowanych algorytmów oraz różnego typu technik algorytmicznych i struktur danych związanych z tekstami (drzewa sufiksowe, tablice sufiksowe, grafy podsłów). Klasyczne problemy algorytmiczne związane są z szukaniem (lub wykrywaniem) wzorca, regularnością i kompresją tekstów. Charakter przedmiotu jest przede wszystkim algorytmiczny, ale zawiera też skromny fragment kombinatoryki tekstów.
Wprowadzimy kilka przykładów ciekawych abstrakcyjnych tekstów, skończonych i nieskończonych. Słowa te są definiowane za pomocą różnego typu operacji: morfizmów, rekurencji, poprzez jakiś wzór itp. Specjalne abstrakcyjne słowa są przedmiotem badań w kombinatoryce słów, ale również są użyteczne przy testowaniu algorytmów tekstowych, tak więc mają one znaczenie praktyczne pomimo ich abstrakcyjności. Poza tym słowa te mają bardzo ciekawe własności związane z różnymi problemami, dla których konstruuje się efektywne algorytmy.
Jeśli \(h : \Sigma \to \Sigma\) jest morfizmem takim, że \(h(a)\) zaczyna się od litery \(a\), to \(h^{\infty}(a)\) jest nieskończonym słowem, którego prefiksami są wszystkie słowa postaci \(h^n(a)\). Piszemy wtedy również \(h^{\infty}(a) = \lim_{n\ \to \infty}\;h^n(a)\).
Słowa tak generowane nazywamy morficznymi. Słowa te są z reguły również definiowane przez rekurencje. Typowymi słowami definiowanymi przez morfizmy i rekurencje są słowa Thue-Morse'a, słowa Fibonacciego i słowa Sturma.
Słowo nazywamy beznakładkowym (bez nakładki) gdy nie zawiera podsłowa postaci \(avava\), gdzie \(a\) jest niepuste. Słowo \(u\) nazywamy kwadratem gdy jest postaci \(u=xx\), dla pewnego niepustego \(x\).
Niech \(Fib_{\infty} = \lim_{n\to \infty}\;Fib_n\) będzie słowem nieskończonym Fibonacciego. Skończone słowa Fibonacciego to
Nieskończone słowa mające tę własność są nazywane słowami Sturma. Są to słowa o minimalnej gęstości podsłów, które nie są okresowe od pewnego miejsca. Zachodzi następujący fakt:
Jeśli dla pewnego \(k\) słowo nieskończone ma co najwyżej \(k\) róznych podsłów długości \(k\), to słowo to jest okresowe od pewnego miejsca.
Nieskończone słowo Thue-Morse'a, które pojawiło się w pracy matematycznej około 100 lat temu i służyło do dowodu tego, że jest nieskończenie wiele tekstów nie zawierających powtórzeń. Słowo to nie zawiera podsłowa postaci \(x^3\), gdzie \(x\) niepuste.
\(Thue_{\infty} = 011010011001011010010110 \ldots \) - nieskończone słowo Thue-Morse'a.
Podobnie definiujemy skończone słowa Thue-Morse'a:
Słowo \(Thue_{\infty}\) możemy również zdefiniować jako nieskończony ciąg binarny, na \(n\)-tym miejscu jest jedynka jeśli liczba jedynek w zapisie binarnym \(n\) jest nieparzysta (numerujemy pozycje od zera).
Słowo \(Thue_{\infty}\) możemy też zdefiniować za pomocą morfizmu: \(h(0)=01,\ h(1)=10\), wtedy \(Thue_{\infty}\ =\ h^{\infty}(a).\)
Najciekawszą własnością słów Thue-Morse'a jest ich beznakładkowość, oraz (w konsekwencji) fakt, że nie zawierają trzecich potęg niepustych słów. Słowa te zawierają kwadraty. Każdy kwadrat w słowie Thue-Morse'a ma rozmiar postaci \(2^k\) lub \(3\cdot 2^k\). Wiele własności tych słów wynika z następującego faktu:
Niech \(Occ(x,\, Thue_{\infty})\) będzie zbiorem początkowych pozycji wystąpień niepustego słowa \(x\) w słowie Thue-Morse'a, wtedy wszystkie elementy zbioru \(Occ(x,\, Thue_{\infty})\) są tej samej parzystości.
Definiujemy słowo bezkwadratowe \(M = 2102012\ldots\) w następujący sposób: kolejną literą jest liczba jedynek między kolejnymi zerami w słowie Thue-Morse'a. Słowo \(M\) nie zawiera podsłowa postaci \(x^2\), gdzie \(x\) niepuste.
Zastanówmy się jak bardzo słowo \(M\) różni się od słowa \(Thue_{\infty}\). Zmieńmy litery w słowie \(M\) zgodnie z następującym morfizmem: \(\mu(s) = (s+1)\ \bmod{3}\). Otrzymujemy ciąg \(\mu(M) = 0210120\ldots\).
Zdefiniujmy słowo \(\bar{B} = 01000101010 \ldots\) jako nieskończony ciąg binarny, w którym na \(n\)-tym miejscu jest jedynka jeśli rozmiar końcowego bloku jedynek w zapisie binarnym \(n\) jest nieparzysty (numerujemy pozycje od zera). Wtedy ciąg \(\mu(M)\) różni się od \(Thue_{\infty}\) dokładnie na tych pozycjach, gdzie \(\bar{B}\) zawiera jedynkę, w tych miejscach ciąg \(M\) zawiera zawsze dwójkę.
Dygresja: podobna konstrukcja w artykule ''Kwadraty'' w Delcie.
Inna konstrukcja tego samego słowa:
w ciągu Thue-Morse każde 00 zamieniamy na 20, każde 11 zamieniamy na 21.
Dygresja: podobna konstrukcja w artykule ''Kwadraty'' w Delcie.
Ciekawymi słowami są ciągi reprezentujące sekwencje ruchów w grze Wieże Hanoi.
\(H\) jest ciągiem ruchów w nieskończonej grze "Wieże Hanoi" (alfabet \( \Sigma = \lbrace \overleftarrow{a},\, \overleftarrow{b},\, \overleftarrow{c},\, a,\, b,\, c \rbrace\)).
W grze tej mamy przenieść \(n\) krążków z wieży o numerze \(i\) na wieżę o numerze \(j\). Wieże są ponumerowane 1, 2, 3.
Przez \(a\), \(b\), \(c\) oznaczmy ruchy przeniesienia górnego krążka z jednej wieży na drugą, odpowiednio \(1 \to 2\), \(2 \to 3\), \(3 \to 1\). A przez \( \overleftarrow{a},\, \overleftarrow{b},\, \overleftarrow{c}\) ruchy odwrotne. Niech \(Han(n,\, i,\, j)\) oznacza minimalny ciąg przenoszący \(n\) krążków z wieży \(i\) na wieżę \(j\). Zachodzi dla \(k\notin \{i,\, j\}\):
$$Han(n,\, i,\, j) = Han(n − 1,\, i,\, k) Han(1,\, i,\, j) Han(n − 1,\, k,\, j).$$
Oznaczmy \(H(n) = Han(n,\, 1,\, 3)\), gdy \(n\) parzyste oraz \(H(n) = Han(n,\, 1,\, 2)\), gdy \(n\) nieparzyste. Wtedy definiujemy \( H = \lim_{n\to \infty}\;H(n)\)
Ciąg \(H\) ma reprezentację poprzez morfizm jak również poprzez pewne rekurencje. W ciągu tym nie ma podsłowa będącego
kwadratem.
\(\bar{K} = 221121221221121122\ldots\) słowo Kolakoskiego, samogenerujące się słowo.
Słowo \(\bar{K}\) spełnia równanie \(r(\bar{K})=\bar{K}\), gdzie \(r(x)\) oznacza ciąg długości kolejnych bloków (maksymalnych spójnych fragmentów zawierających takie same litery), np. \(r(1133322) = 2\;3\;2\).
Niech \(g(k)\) będzie liczbą jedynek w zapisie binarnym liczby \(k\).
Przykład. \(g(21)=3\) gdyż \(21=[10101]_2\) ma 3 jedynki w zapisie binarnym.
Ciekawymi tekstami mającymi związek z tą funkcją są słowa binarne \(P_n\), gdzie \(P_n\) jest \(n\)-tym wierszem trójkąta Pascala modulo 2.
$$P_n(i) = {\left(\begin{array}{c}n\\i\end{array}\right)} \mod 2.$$
Można potraktować te słowa jako nieskończone (w prawo) dopisując dużo zer, wtedy
słowo \(P_0\ =\ x\ =\ 1000...\). Każde nastepne słowo powstaje z poprzeniego stując jednocześnie do
wszystkich pozycji \(i\) w słowie (potencjalnie) nieskończonym (poza pozycją pierwszą) $$x_i=(x_{i-1}+x_i) \ modulo \ 2$$
Mamy:
$$P_{0}=1, \
P_{1}=11,\
P_{2}=101,\
P_{3}=1111,\
P_{4}=10001,\
P_{5}=110011$$
Słowa \(P_n\) mają następującą ciekawą własność: liczba jedynek w \(P_n\) jest równa \(2^{g(n)}\).
Niech \(W(n)\) oznacza zbiór pozycji zawierających jedynkę w zapisie binarnym liczby \(n\). Zachodzi:
$$P_n(k) = 1 \Leftrightarrow W(k) \subseteq W(n)$$
Rozważmy nieskończony ciąg, który jest konkatenacją kolejnych liczb naturalnych zapisanych w systemie dzisiętnym (zero zapisujemy jako \(0\)). Zatem
$$W\ =\ 012345678910111213141516171819202122232425262728293031323334353637\ldots$$
Niech \(W_n\) oznacza prefiks tego ciągu długości \(n\). Słowa \(W_n\) mają następującą ciekawą własność: dla każdych dwóch niepustych słów tej samej długości \(x\), \(y\) zachodzi
$$\lim_{n \rightarrow \infty} \frac{|Occ(x,\, W_n)|}{|Occ(y,\,W_n)|} = 1.$$
Oznacza to, że w pewnym sensie ciąg \(W\) jest losowy.
Binarnym słowem de Bruijna rzędu \(k\) nazywamy każde słowo \(x\) długości \(2^k\), które zawiera każde słowo binarne długości \(k\) jako słowo cykliczne. Inaczej mówiąc, jeśli dopiszemy do \(x\) na końcu jego prefix długości \(k - 1\). to otrzymane słowo będzie zawierać każde słowo długości \(k\) w sensie klasycznego słowa liniowego. Oczywiście najkrótsze słowo liniowe zawierające każde słowo długości \(k\) musi mieć długość co najmniej \(2^k + k - 1\), czyli słowa de Bruijna realizują to minimum.
Dla każdego \(k\) istnieje wykładniczo dużo względem \(2^k\) słów de Bruijna rzędu \(k\). Dla \(k=1\) możemy wziąć słowo \(ab\), jako słowo de Bruijna, dla \(k=2\) słowem de Bruijna jest \(aabb\), natomiast \(aaababbb\) jest słowem de Bruijna rzędu 3. Zauważmy, że wszystkie te słowa są leksykograficznie pierwszymi słowami de Bruijna danego rzędu. W jednym z następnych rozdziałów podamy efektywne algorytmy generowania leksykograficznie pierwszych ciągów de Bruijna.
Mamy alfabet trzyliterowy \(\{a,b,c\}\). Konstruujemy słowo bezkwadratowe wypisująć kolejne litery. Bierzemy zawsze kolejną literę różną od poprzedniej. Daje to zawsze dwie możliwości wyboru.
Aby zdeterminować konstrukcję i uzyskać poprawny ciąg dodajmy warunek, że każda kolejna litera musi być różna od środkowej litery dotychczasowego słowa. Dokładniej, przy wyznaczaniu \(i\)-tej litery interesuje nas litera o indeksie \(\lfloor i/2 \rfloor\) przy czym litery słowa numerujemy od zera.
Jeżeli to kryterium wciąż dopuszcza dwie możliwości, to wybieramy tę spośród niezabronionych liter, która występuje wcześniej w alfabecie. W ten sposób otrzymujemy takie oto słowo:
$$abacbcabacabcbacb\ldots$$
Okazuje się, że dowolnie długie słowo wygenerowane w ten sposób jest bezkwadratowe.
W module tym zakładamy elementarną znajomość pojęć z przedmiotu: Automaty, języki i obliczenia.
Automaty skończone są modelem bardzo uproszczonych algorytmów tekstowych, automat czyta on-line tekst i za każdym razem daje odpowiedź, czy wczytany dotychczas prefiks ma odpowiednią własność. W ten sposób automat rozwiązuje problem string-matching otrzymując na wejściu strumień symboli tekstu, w którym szukamy wzorca. Ten tekst nie jest zapamiętywany.
W naszym przypadku interesować nas będzie to, czy aktualnie wczytany tekst kończy się wystąpieniem wzorca, inaczej mówiąc, czy automat akceptuje język \(\Sigma^*x\), gdzie \(\Sigma\) jest alfabetem wejściowym, a \(x\) jest wzorcem.
Deterministyczny automat skończony jest formalnie zadany (wyspecyfikowany) jako
$$A = (\Sigma,\, Q,\, \delta,\, q_0,\, F), $$
gdzie \(Q\) jest skończonym zbiorem stanów, \(q_0\) jest stanem początkowym, \(F\) jest tzw. zbiorem stanów akceptujących (ale nie kończących obliczenie - w definicji automatu nie występuje pojęcie stopu).
Algorytm znajdowania wystąpień wzorca wygąda następująco:
repeat
if
\(q \in F\) then
wypisz kolejne wystąpienie wzorcaZamiast \(q' = \delta(q,\, \alpha)\) będziemy też pisać \(q \stackrel{\alpha}{\rightarrow} q'\).
Niech \(w\) będzie wzorcem. Deterministyczny automat \(A_w\) akceptujący język \(\Sigma^* w\) można skonstruować biorąc za zbiór stanów \(Q\) wszystkie prefiksy słowa \(w\) (\(A\) ma zatem \(\vert w \vert + 1\) stanów) oraz przejścia \(v \stackrel{\alpha}{\rightarrow} u\), gdzie \(u\) jest maksymalnym właściwym sufiksem słowa \(v \alpha\) będącym jednocześnie prefiksem \(w\). Definiujemy:
Niech \(P\) będzie tablicą prefikso-sufiksów dla \(w\), przy czym będziemy indeksować tablicę prefiksami \(w\). Wtedy konstruujemy funkcję \(\delta\) (program automatu) następująco:
for
\(q \in Q\), \(q\) niepuste, w kolejności rosnących długości \(q\) do
if
\(u \alpha \in Q\) then
\(\delta(u,\, \alpha) := u\alpha\)else
\(\delta(u,\, \alpha) := \delta(P[u],\, \alpha)\)Udowodnić, że jeśli w automacie dla jednego wzorca usuniemy tranzycje prowadzące do stanu \(q_0\), to mamy co najwyżej \(2n-1\) tranzycji. Dowieść, że liczba nietrywialnych przejść "wstecznych", tj. przejść postaci \(v \stackrel{\alpha}{\rightarrow} u\), gdzie \( \vert v \vert > \vert u \vert > 0\), jest nie większa niż \(\vert w \vert\). Zauważmy, że daje to możliwość reprezentacji automatu o rozmiarze proporcjonalnym do \(\vert w \vert\) (przejścia postaci \(v \stackrel{\alpha}{\rightarrow} \epsilon\) możemy pominąć).
Wskazówka: wykazać, że dla każdej liczby \(k\) istnieje co najwyżej jedno nietrywialne przejście wsteczne, takie, że \(\vert v \alpha \vert - \vert u \vert = k\).
Załóżmy, że dla każdego stanu automatu trzymamy "tranzycje" w liście posortowanej względem "długości" stanu, do którego tranzycja prowadzi. Mając taki automat możemy wykonywać string-matching brutalnie, w danym momencie szukamy tranzycji dla danej litery przeszukując listę liniową. Udowodnić, że daje to liniowy, niezależny od rozmiaru alfabetu, algorytm dla string-matchingu. Algorytm ten jest znany jako algorytm Simona.
Załóżmy, że mamy teraz skończony zbiór \(X\) wzorców o łącznej sumie długości \(n\). Załóżmy dla uproszczenia, że żaden wzorzec ze zbioru \(X\) nie jest prefiksem innego wzorca z \(X\). Konstruujemy automat akceptujący język \(\Sigma^*X\).
Automat dla \(\Sigma^*X\) możemy konstruować podobnie jak dla jednego wzorca. W tym wypadku:
Przejście \(\delta\) jest w szkielecie "do przodu" jeśli z prefiksu \(u\) prowadzi do pewnego prefiksu \(u \alpha\). Pozostałe przejścia są "w tył".
Pierwsza metoda polega na wstępnym obliczeniu uogólnionej tablicy \(P\) prefisko-sufisków, a następnie za jej pomocą skonstruowanie automatu podobnie jak to wcześniej robiliśmy. \(P[u]\) jest najdłuższym właściwym sufiksem \(u\) będącym prefiksem pewnego wzorca ze zbioru \(X\).
Skonstruować algorytm obliczający tablicę \(P\) dla wielu wzorców w czasie \(O(\vert x_1 \vert + \vert x_2 \vert + \ldots \vert x_k \vert)\) gdzie \(x_1,\, x_2, \ldots x_k\) są wzorcami.
Mając tę tablicę konstruujemy funkcję \(\delta\) (program automatu) następująco:
for
\(q \in T\), \(q\) niepuste, w kolejności BFS na drzewie \(T\) do
if
\(u \alpha \in T\) then
\(\delta(u,\, \alpha) := u \alpha\)else
\(\delta(u,\, \alpha) := \delta(P[u],\, \alpha)\)Dla wielu wzorców "bezpośrednia" metoda polega na przechodzeniu drzewa \(T\) prefiksów wzorców metodą BFS. Na początku tworzymy automat poziomu zerowego, jest to zapętlony (dla każdego symbolu) korzeń drzewa \(T\).
for
\(u \alpha \in T\), \(\alpha \in \Sigma\), w kolejności BFS na drzewie \(T\) do
if
\(w \beta\) jest prefiksem pewnego wzorca then \(\delta(u \alpha,\, \beta) := w \beta\)else
\(\delta(u \alpha,\, \beta) := \delta(w,\, \beta)\).
Na przykład jeśli zbiór wzorców jest \(\lbrace abab,\, babb,\, bba \rbrace\) i mamy automat dla zbioru z poziomu drugiego \(\lbrace ab,\, ba,\, bb \rbrace\) to automat dla następnego poziomu możemy zacząć od liczenia tranzycji dla stanu odpowiadającego prefiksowi \(bab\), tworząc nowy stan odpowiadający prefiksowi \(bab\), kopiując przejścia dla nowego stanu ze stanu \(w = ab\), do którego prowadzi przejście etykietowane \(b\) ze stanu \(u_i = ba\), poza przejściem \(\delta(bab,\, a) = w \; a)\) gdyż \(w\; a\) jest prefiksem pewnego wzorca.
Jeśli mamy dwa automaty dające algorytmy dla tego samego problemu, to chcielibyśmy móc szybko sprawdzić, czy automaty realizują tę samą funkcję: wejście \(\Rightarrow\) wyjście. Dla mocniejszych modeli obliczeniowych nie ma algorytmów na sprawdzanie tego typu równoważności. Deterministyczne automaty skończone są użyteczne też dlatego, że są one algorytmicznie łatwe.
Zamiast sprawdzać równoważność dwóch automatów łatwiej sprawdza się równoważność dokładnie dwóch stanów \(q_1\), \(q_2\) tego samego automatu. W algorytmie używamy struktury danych dla problemu FIND-UNION. Jeśli zaczynamy od podziału trywialnego (każdy element w klasie jednoelementowej) to \(n\) operacji \(find\) i \(union\) możemy wykonać w łącznym czasie \(O(n \log^*n)\) (odsyłamy do przedmiotu ASD).
while
kolejka niepusta do
if
\(A \neq B\) then
Udowodnić poprawność tego algorytmu. Algorytm działa w czasie \(O(n \log^*n)\), jeśli alfabet jest stałego rozmiaru.
Innym problemem teorio-automatowym, dla którego istnieje efektywny algorytm jest problem minimalizacji. Można ten problem rozwiązać w czasie \(O(n \log n)\), ale algorytm jest dosyć wyrafinowany. Pokażemy niezmiernie prosty algorytm działający w czasie \(O(n^2)\), redukując w bardzo prosty sposób nasz problem do problemu teoriografowego: przechodzenie grafu skierowanego.
Tworzymy graf \(G\), gdzie wierzchołki budujemy z par stanów \((\delta(s,\, \alpha),\; \delta(s',\, \alpha)) \rightarrow (s,\, s')\) dla każdej pary stanów \(s,\, s'\) i symbolu \(\alpha \in \Sigma\). Stany \(s,\, s'\) nie są równoważne wtedy i tylko wtedy, gdy istnieje w \(G\) ścieżka od \(F \times (Q-F) \cup (Q-F) \times F\) do \((s,\, s')\). Daje to algorytm \(O(n^2)\) na minimalizację automatu.
Jeśli policzyliśmy relację równoważności dla wszystkich par stanów to sklejamy klasy równoważnych stanów, w rezultacie otrzymujemy automat minimalny (złożony ze stanów osiągalnych ze stanu początkowego).
Mówimy, że dwa stany są \(i\)-równoważne, gdy są równoważne z dokładnością do słów długości co najwyżej \(i\). Niech \(R_i\) oznacza relację \(i\)-równoważności. Udowodnić, że \(R_i = R_{i+1}\) implikuje \(R_i = R_{i+2}\) oraz \(R_i\) jest końcową relacją równoważności stanów. Wynika stąd, że dwa stany są równoważne gdy są równoważne z dokładnością do słów (wejść automatu) długości co najwyżej \(n\), gdzie \(n\) jest liczbą stanów automatau.
W praktyce często szukamy wzorców w sposób aproksymacyjny. Pierwszą miarą aproksymacji tekstów jest tzw. odległość edycyjna, oznaczona przez \(edit(x,\, y)\). Odległość ta jest równa minimalnej liczbie operacji usunięcia, wstawienia lub zamiany pojedyńczego symbolu, które należy wykonać na podanych słowach, by doprowadzić je do jednakowej postaci. Funkcja \(edit\) ma następujące własności:
Tekst \(x = abab\) można zamienić na \(y = baabc\) za pomocą dwóch operacji zamiany i jednej operacji wstawienia symbolu. Tak więc \(edit(abab,\, baabc) = 3\).
Ustalmy teksty \(x\) (\(\vert x \vert = m\)) i \(y\) (\(\vert y \vert = n\)). Definiujemy następującą tablicę dwuwymiarową:
$$ED[i,\, j] = edit(x[1 \ldots i],\, y[1 \ldots j])$$
dla \(0 < i \leq m\), \(0 < j \leq n\). Wartości brzegowe tablicy ustalamy następująco:
$$\left( \forall\; 0 \leq i \leq m,\, 0 \leq j \leq n \right) \quad ED[i,\, 0] = i,\; ED[0,\, j] = j.$$
Pozostałe elementy tablicy liczymy korzystając z programowania dynamicznego:
$$ED[i,\, j] = \min \lbrace ED[i-1,\, j] + 1,\; ED[i,\, j-1] + 1,\; ED[i-1,\, j-1] + \partial (x[i],\, y[j]),$$
gdzie \(\partial (\alpha,\, \beta) = 0\) gdy \(\alpha = \beta\), oraz \(\partial(\alpha,\, \beta) = 1\) w przeciwnym przypadku.
Kolejne wiersze tablicy \(ED\) możemy liczyć pamiętając jedynie ostatni wiersz, ponieważ musimy policzyć jedynie element \(ED[m,\, n]\). Zatem nie musimy trzymać w pamięci całej tablicy. Odległość edycyjną dwóch tekstów o długościach \(n\) i \(m\) może być obliczona w czasie \(O(mn)\) korzystając z dodatkowej pamięci rozmiaru \(O(\min \lbrace m,\, n \rbrace)\).
Jeśli chcemy policzyć najkrótszą sekwencję operacji zamieniających jeden tekst w drugi, to potrzebna nam jest cała tablica \(ED\). Sekwencję taką liczymy cofając się od elementu \(ED[m,\, n]\).
Problem string-matchingu z błędami różni się tylko nieznacznie od liczenia odległości edycyjnej. Problem polega na policzeniu wartości \(\min (edit(x,\, y) : y \in Subwords(y))\). Jednocześnie chcielibyśmy policzyć podsłowo w \(y\) jak najbliższe danemu wzorcowi \(x\).
Definiujemy tablicę:
$$SMED[i,\,j] = \min ( edit(x[1 \ldots i],\, y) : y \in Subwords(y[1 \ldots j]) ).$$
Twierdzenie: problem string-matchingu z błędami można rozwiązać w czasie \(O(mn)\).
Dowód
Zasadniczą róźnicą między obliczaniem \(ED\) i \(SMED\) jest inicjalizacja, gdyż pusty prefiks wzorca "pasuje" do pustego podsłowa tekstu \(y\):
$$\left( \forall\; 0 \leq i \leq m,\, 0 \leq j \leq n \right) \quad SMED[i,\, 0] = i,\; SMED[0,\, j] = 0.$$
Wzory rekurencyjne liczenia "wewnętrznych" elementów tablicy \(SMED\) są takie same jak dla tablicy \(ED\).
Załóżmy, że pewne pozycje wzorca są nieistotne - będziemy mówić, że zawierają one symbol \(\diamondsuit\) będący dziurą lub tzw. symbolem uniwersalnym (ang. don't care symbol). Symbol \(\diamondsuit\) pasuje do każdego innego symbolu, włącznie ze sobą. Piszemy \(\alpha \approx \beta\) gdy symbole \(\alpha\) i \(\beta\) są równe lub co najmniej jeden jest dziurą.
Słowa \(u\), \(v\) (tej samej długości \(n\)) są prawie równe, gdy \(u[i] \approx v[i]\) dla każdej pozycji \(i\); piszemy wtedy \(u \approx v\). Wyszukiwanie wzorca z dziurami polega na znalezieniu pozycji w tekście \(y\) takich, że \(y[i..i+m-1] \approx x\), gdzie \(x\) jest wzorcem z dziurami długości \(m\).
Twierdzenie
Jeśli jedyną informacją z danych wejściowych jest porównanie dwóch symboli za pomocą operacji \(\approx\), to dla pewnych tekstów wejściowych potrzeba \(\Omega(n^{2})\) porównań \(\approx\) do rozwiązania problemu string-matchingu z dziurami.
Dowód
Rozważmy \(x = \diamondsuit^m\), \(y = \diamondsuit^{2m}\). W tym przypadku wzorzec występuje (zaczyna się) na wszystkich pozycjach w przedziale \([1..m]\). Jeśli nie wykonamy jakiegoś porównania między symbolami na pozycjach \(i,\, j\) to zmieniając zawartość tych pozycji na dwa różne symbole zmienimy wyjście nie zmieniając informacji, z której korzystamy na wejściu. Prowadzi to do sprzeczności z poprawnością algorytmu.
Załóżmy, że pozycje słów \(x,\, y\) są teraz ponumerowane od zera. Zdefiniujmy operację \(\odot\) w następujący sposób. Niech \(z = x \odot y\) będzie słowem długości \(m+n-1\), w którym wartością na \(k\)-tej pozycji, dla \(k = 0,\, 1, \ldots,\, m+n-2\), jest wartość:
$$z[k] = \bigwedge_{i+j = k}\; x[i] \approx y[j]$$
Operacja \(\bigwedge\) oznacza tutaj koniunkcję logiczną wielu czynników. Symbolicznie zapisujemy:
$$\odot =(\approx ,\textrm{ and})$$
Jak zwykle \(x^R\) oznacza odwrócenie słowa \(x\). Rozważmy "iloczyn" \(z = x^R \odot y\). Zachodzi
$$z[k] = 1 \ \Leftrightarrow\ x^R[m-1] \approx y[k-m+1] \wedge x^R[m-2] \approx y[k-m+2] \wedge \ldots \wedge x^R[0] \approx y[k] $$
Zatem każda sytuacja \(z[k] = 1\) odpowiada wystąpieniu wzorca \(x\), które kończy się na pozycji \(k\) w \(y\). Tak więc string-matching z dziurami sprowadza się do obliczania iloczynu \(\odot\).
Zamiast bezpośrednio liczyć operację \(\odot\) zdefiniujemy obliczeniowo łatwiejsze operacje. Dla wektorów logicznych \(x,\, y\) definiujemy operację \(z = x \otimes y\) jako:
$$z[k] = \bigvee_{i+j = k} \; x[i] \wedge y[j]$$
Operacja \(\bigvee\) oznacza tutaj alternatywę wielu składników logicznych. Dla słowa \(x\) i symbolu \(\alpha\), oznaczamy przez \(\mathit{logical}(\alpha,\, x)\) wektor logiczny, którego \(i\)-ta współrzędna jest \(true\) wtedy i tylko wtedy, gdy \(x[i] = \alpha\). Zdefiniujmy również
$$\mathit{LOGICAL}_{\alpha,\, \beta}(x,\, y) = \mathit{logical}(\alpha,\, x) \otimes logical(\beta,\, y).$$
Zachodzi teraz następujący oczywisty fakt:
$$x \odot y = \textrm{not}\;\left(\; \bigvee_{\alpha \neq \beta,\; \alpha,\, \beta \in \Sigma} \mathit{LOGICAL}_{\alpha,\, \beta}(x,\, y)\;\right)$$
Zatem dla ustalonego alfabetu złożoność operacji \(\odot\) jest tego samego rzędu, co operacji \(\otimes\). W poniższym twierdzeniu pokażemy jaki to ma związek ze złożonością mnożenia liczb naturalnych zapisanych w systemie binarnym.
Twierdzenie
Problem string-matchingu z dziurami można rozwiązać w czasie \(O(n\log^3 n)\) dla alfabetu o rozmiarze \(O(1)\).
Dowód
Niech \(x,\, y\) będą dwoma logicznymi (binarnymi) ciągami długości \(n\) oraz niech \(k = \lceil \log n \rceil\)). Zastępujemy w \(x,\, y\) wartości logiczne \(true\), \(false\) przez (odpowiednio) jedynki i zera. Wstawmy blok \(k\) zer pomiędzy każde dwa elementy ciągów \(x,\, y\). W ten sposób otrzymujemy binarne reprezentacje dwóch liczb \(x'\) i \(y'\).
Ciąg \(z = x \otimes y\) możemy odtworzyć z ciągu binarnego odpowiadającego standardowemu mnożeniu \(x \times y\) w następujący sposób. Bierzemy kolejne bloki długości \(k+1\) w \(x \times y\). Jeśli taki blok odpowiada liczbie niezerowej, to kolejnym elementem \(x \odot y\) będzie 1, w przeciwnym wypadku zero. W ten sposób zredukowaliśmy problem liczenia operacji \(\odot\) do mnożenia liczb binarnych długości \(O(n\log n)\). Mnożenie liczb binarnych długości \(k\) można wykonać w czasie \(O(k \log^2 k)\) za pomocą algorytmu Schönhage-Strassena, zatem koszt operacji \(\otimes\) to \(O(n \log^2 n )\). Stąd i z redukcji operacji \(\odot\) do \(\otimes\) wynika teza twierdzenia.
Dygresja: mnożenie liczb można wykonać trochę szybciej niż \(O(k \log^2 k)\) używając niezmiernie skomplikowanych algorytmów algebraicznych.