From b8f32cc89894f5c2da94c32beb022e3c92ee15d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojte=CC=8Cch=20Struha=CC=81r?= Date: Wed, 11 Jun 2025 22:42:54 +0200 Subject: [PATCH] Remove algo string matching --- src/content/docs/szmgr/SZP01_algoritmy.md | 243 ---------------------- 1 file changed, 243 deletions(-) diff --git a/src/content/docs/szmgr/SZP01_algoritmy.md b/src/content/docs/szmgr/SZP01_algoritmy.md index 5262a32..6075ba8 100644 --- a/src/content/docs/szmgr/SZP01_algoritmy.md +++ b/src/content/docs/szmgr/SZP01_algoritmy.md @@ -388,249 +388,6 @@ k + (\|S\| - k) - \|S\| = 0 & \text{pokud } \|S\| > k \\ \end{cases} ``` -## Vyhledávání řetězců (string matching) - -_String matching_ označuje rodinu problémů obsahující třeba: - -- Nalezení prvního přesného výskytu podřetězce (_patternu_) v řetězci (_stringu_ / _textu_). -- Nalezení všech výskytů podřetězce v řetězci. -- Výpočet vzdálenosti dvou řetězců. -- Hledání opakujících se podřetězců. - -Většinou je řetězec polem znaků z konečné abecedy $\Sigma$. String matching algoritmy se ale dají použít na ledacos. - -Vzorek $P$ se vyskytuje v textu $T$ s posunem $s$, pokud $0 \le s \le n - m$ a zároveň $T\lbrack (s+1) .. (s + m) \rbrack = P$. Pro nalezení platných posunů lze použít řadu algoritmů, které se liší složitostí předzpracování i samotného vyhledávání: [^iv003-strings] - -| Algoritmus | Preprocessing | Searching | -| ----------------------------------- | ------------------------------------ | ----------------------------------- | -| Brute force / naivní | $0$ | $\mathcal{O}((n - m + 1) \cdot m)$ | -| Karp-Rabin | $\Theta(m)$ | $\mathcal{O}((n - m + 1) \cdot m)$ | -| finite automata | $\Theta(m \cdot \vert \Sigma \vert)$ | $\Theta(n)$ | -| Knuth-Morris-Pratt | $\Theta(m)$ | $\Theta(m)$ | -| Boyer-Moore | $\Theta(m + \vert \Sigma \vert)$ | $\mathcal{O}((n - m + 1) \cdot m)$ | - -- $T$ nebo $T\lbrack 1..n \rbrack$ -- text. -- $P$ nebo $P\lbrack 1..m \rbrack$ -- pattern. -- $n$ -- délka textu $T$. -- $m$ -- délka vzorku / podřetězce / patternu $P$. -- $\Sigma$ -- konečná abeceda, ze které je složen text i pattern. - -### Brute force / naivní - -Prochází všechny pozice v textu a porovnává je s patternem. Pokud se neshodují, posune se o jedno pole dopředu. - -Pokud se text neshoduje už v prvním znaku, je složitost lineární. Avšak v nejhorším případě, kdy se pattern shoduje s textem až na poslední znak, je složitost až kvadratická. - -```csharp -int Naive(string text, string pattern) -{ - int n = text.Length; - int m = pattern.Length; - for (int i = 0; i < n - m + 1; i++) - { - // NB: Substring creates a new string but let's not worry about that. - if (text.Substring(i, m) == pattern) - { - return i; - } - } - return -1; -} -``` - -- **Složitost**\ - Cyklus je proveden $n-m+1$-krát. V každé iteraci se provede přinejhorším až $m$ porovnání. (Zanedbáváme složitost `Substring`, která v C# dělá kopii.) - -### Karp-Rabin - -Používá hashování. Vytvoří hash z patternu a hashuje i všechny podřetězce délky $m$ v textu. Nejprve eliminuje všechny pozice, kde se hashe neshodují. Pokud se shodují, porovná je znak po znaku. - -```csharp -int KarpRabin(string text, string pattern) -{ - int n = text.Length; - int m = pattern.Length; - int patternHash = pattern.GetHash(); - for (int i = 0; i < n - m + 1; i++) - { - // NB: Assume that `GetHash` is a rolling hash, even though in .NET it's not. - if (text.Substring(i, m).GetHash() == patternHash) - { - if (text.Substring(i, m) == pattern) - { - return i; - } - } - } - return -1; -} -``` - -- **Hashování**\ - Trik spočívá v použití _rolling_ hashovacího algoritmu. Ten je schopný při výpočtu hashe pro $T\lbrack s .. (s + m) \rbrack$ použít hash $T\lbrack s .. (s + m - 1) \rbrack$ s využitím pouze jednoho dalšího znaku $T\lbrack s + m \rbrack$. Přepočet hashe je tedy $\mathcal{O}(1)$. - - Jeden takový algoritmus lze získat použitím Hornerova schématu pro evaluaci polynomu. [^horner] Předpokládejme, že $\Sigma = \{0, 1, ..., 9\}$ (velikost může být libovolná), pak pro hash $h$ platí - - ```math - \begin{align*} - h &= P[1] + 10 \cdot P[2] + 10^2 \cdot P[3] + ... + 10^{m-1} \cdot P[m] \\ - &= P[1] + 10 \cdot (P[2] + 10 \cdot (P[3] + ... + 10 \cdot P[m] ... )) - \end{align*} - ``` - - Pokud jsou hashe příliš velké, lze navíc použít modulo $q$, kde $10 \cdot q \approx \text{machine word}$. - -- **Složitost**\ - Předzpracování zahrnuje výpočet $T \lbrack 1..m \rbrack$ v $\Theta(m)$. - - Složitost výpočtu je v nejhorším případě $\mathcal{O}((n - m + 1) \cdot m)$, jelikož je potřeba porovnat všechny podřetězce délky $m$ s patternem. - - Tento algoritmus se hodí použít, pokud hledáme v textu celé věty, protože neočekáváme velké množství "falešných" shod, které mají stejný hash jako $P$. V tomto případě je průměrná složitost $\mathcal{O}(n)$. - -### Konečné automaty - -Složitost naivního algortmu lze vylepšit použitím konečného automatu. - -Mějmě DFA $A = (\{0, ..., m\}, \Sigma, \delta, \{0\}, \{m\})$, kde přechodobou funkci definujeme jako: - -```math -\delta(q, x) = \text{největší } k \text{ takové, že } P[1..k] \text{ je \textbf{prefix} a zároveň \textbf{suffix} } P[1..q] . x -``` - -Jinými slovy, $\delta$ vrací delků nejdelšího možného začátku $P$, který se nachází na daném místě (stavu $q$) v řetězci $T$. - -Prakticky by příprava přechodové funkce mohla vypadat takto: - -```csharp -int[,] CreateAutomaton(string pattern) -{ - int m = pattern.Length; - // NB: Assumes that the alphabet is ASCII. - int[,] automaton = new int[m + 1, 256]; - for (int q = 0; q <= m; q++) - { - for (int c = 0; c < 256; c++) - { - int k = Math.Min(m + 1, q + 2); - do - { - k--; - } - while (!pattern.Substring(0, q).Equals(pattern.Substring(0, k) + (char)c)); - automaton[q, c] = k; - } - } - return automaton; -} -``` - -Vyhledávání v textu pak bude vypadat takto: - -```csharp -int FiniteAutomaton(string text, string pattern) -{ - int n = text.Length; - int m = pattern.Length; - int[,] automaton = CreateAutomaton(pattern); - int q = 0; - for (int i = 0; i < n; i++) - { - q = automaton[q, text[i]]; - if (q == m) - { - return i - m + 1; - } - } - return -1; -} -``` - -Tato metoda šetří čas, pokud se pattern v některých místech opakuje. Mějmě například pattern `abcDabcE` a text `abcDabcDabcE`. Tato metoda nemusí začínat porovnávat pattern od začátku po přečtení druhého `D`, ale začne od $P \lbrack 5 \rbrack$ (včetně), protože _ví_, že předchozí část patternu se již vyskytla v textu. - -Jinými slovy na indexu druhého `D` je `abcD` nejdelší prefix $P$, který je zároveň suffixem už načteného řetězce. - -- **Složitost**\ - Vytvoření automatu vyžaduje $\Theta(m^3 \cdot |\Sigma|)$ času, dá se však provést efektivněji než v `CreateAutomaton` a to v čase $\Theta(m \cdot |\Sigma|)$. - - Složitost hledání je pak v $\Theta(n)$. [^iv003-strings] - -### Knuth-Morris-Pratt (KMP) - -KMP představuje efektivnější využití idei z metody konečného automatu: - -- Každý stav $q$ je označen písmenem z patternu. Výjimkou je počáteční stav $S$ a koncový stav $F$. -- Každý stav má hranu `success`, která popisuje sekvenci znaků z patternu, a `failure` hranu, která míří do některého z předchozích stavů -- takového, že už načtené znaky jsou největší možný prefix patternu. - -V reálné implementaci nejsou `success` hrany potřeba; potřebujeme jen vědět, kam skočit v případě neúspěchu. - -```csharp -/// -/// Computes the longest proper prefix of P[0..i] -/// that is also a suffix of P[0..i]. -/// -int[] ComputeFailure(string pattern) -{ - int m = pattern.Length; - int[] fail = new int[m]; - int j = 0; - for (int i = 1; i < m; i++) - { - while (j >= 0 && pattern[j] != pattern[i]) - { - j = fail[j]; - } - - // If comparison at i fails, - // return to j as the new starting point. - fail[i] = j; - - j++; - } - return fail; -} - -int KnuthMorrisPratt(string text, string pattern) -{ - int[] fail = ComputeFailure(pattern); - int n = text.Length; - int m = pattern.Length; - // NB: I index from 0 here. Although I use 1..n in the text. - int i = 0; - int j = 0; - for (int i = 0; i < n; i++) - { - while (j >= 0 && text[i] != pattern[j]) - { - /* - There can be at most n-1 failed comparisons - since the number of times we decrease j cannot - exceed the number of times we increment i. - */ - j = fail[j]; - } - - j++; - if (j == m) - { - return i - m; - } - } - return -1; -} -``` - -> [!WARNING] -> Nejsem si jistý, že ty indexy v kódu výše mám dobře. - -> [!NOTE] -> "In other words we can amortize character mismatches against earlier character matches." [^iv003-strings] - -- **Složitost**\ - Amortizací neúspěšných porovnání vůči úspěšným získáme $\mathcal{O}(m)$ pro `ComputeFailure` a $\mathcal{O}(n)$ pro `KnuthMorrisPratt`. - [^iv003]: [IV003 Algoritmy a datové struktury II (jaro 2021)](https://is.muni.cz/auth/el/fi/jaro2021/IV003/) -[^iv003-strings]: https://is.muni.cz/auth/el/fi/jaro2021/IV003/um/slides/stringmatching.pdf -[^rabin-karp-wiki]: https://en.wikipedia.org/wiki/Rabin%E2%80%93Karp_algorithm -[^horner]: https://en.wikipedia.org/wiki/Horner%27s_method [^backtracking]: https://betterprogramming.pub/the-technical-interview-guide-to-backtracking-e1a03ca4abad