De presentatie wordt gedownload. Even geduld aub

De presentatie wordt gedownload. Even geduld aub

String Matching Algoritmiek.

Verwante presentaties


Presentatie over: "String Matching Algoritmiek."— Transcript van de presentatie:

1 String Matching Algoritmiek

2 Algoritmiek: Divide & Conquer
String Matching Gegeven string (haystack): aabaabbabaaba zoek patroon abba (needle) 4 algoritmen: Naïef Rabin-Karp Eindige Automaat Knuth-Morris-Pratt String Matching is de functionaliteit die je in veel programma’s van control+f krijgt. Dit college gaat over exact matchen, je kan ook approximate matching doen (maar niet met deze algoritmen): bijvoorbeeld als je wil weten of een DNA profiel (ongeveer) overeenkomt. Algoritmiek: Divide & Conquer

3 String Matching (formeel)
Tekst 𝐴[0…𝑛−1], patroon 𝑃[0…𝑚−1] Wil: 𝑘 zodat 𝐴 𝑘…𝑘+𝑚 =𝑃[0…𝑚−1] Tekst is over alfabet 𝚺 ({0,1}, {𝐴,𝐵,…,𝑍}, [0,255], o.i.d.) Substring met lengte 𝑚 van 𝐴 die begint op positie 𝑘 Algoritmiek: Divide & Conquer

4 Algoritmiek: Divide & Conquer
Naïef NaiveMatch(A,P) for i := 0 to n-m-1 if Checkmatch(A,i,P) yield i return CheckMatch(A,k,P) for i := 0 to m-1 if A[k+i] ≠ P[i] return false return true Bekijk alle mogelijke shifts Looptijd? Als je geluk hebt, returned CheckMatch snel: als de strings random zijn (bijvoorbeeld random strings uit [a,b,c,…,z]*) dan gebeurt dat verwacht na 26/25 pogingen (want kans van 25/26 dat er geen overeenkomst is). Dan is NaiveMatch O(n). Het wordt vervelend als er vaak een match is, bij de string abababababab matchen met abab kost iedere even shift het maximale aantal vergelijkingen O(nm). Het kan ook vervelend zijn als er niet zo vaak (of zelfs nooit) matches zijn, bijvoorbeeld de string aaaaaaaaaa met pattern aaab. Die O(nm) kan acceptabel zijn, zeker als het patroon klein is (en in de praktijk gaat het vaak veel sneller). Toch is O(nm) worst-case voor veel toepassingen niet acceptabel. Het eerste geval is iets acceptabeler: de tijd per match is O(m). Ik bedoel met yield i wat in C# yield return heet, en met return bedoel ik yield break. Algoritmiek: Divide & Conquer

5 Algoritmiek: Divide & Conquer
Rabin-Karp Idee: vat strings op als getallen in base− Σ hallo is base 26 (= ) 00101 is base 2 (=20) Vergelijk getallen ipv. strings Algoritmiek: Divide & Conquer

6 Algoritmiek: Divide & Conquer
Voorbeeld Zoek orit in algoritme orit =15⋅ ⋅ ⋅ =276062 algo = 1⋅ ⋅26+7 ⋅26+15=25885 lgor = 12⋅26 +7 ⋅26+15 ⋅26+18=216052 gori= 7⋅ ⋅26+18 ⋅26+9=133649 orit= 15⋅ ⋅26+9 ⋅26+20=276062 Algoritmiek: Divide & Conquer

7 Algoritmiek: Divide & Conquer
Rabin-Karp (1) RK1(A,P) H = String2Int(P) for i := 0 to n-m-1 if String2Int(A[I…i+s]) = H yield i return String2Int(A) r = h for i := 0 to n-1 h := Σ h + A[i] return h Looptijd? Dit schiet qua looptijd helemaal niks op, want String2Int kost O(m), totaal is O(nm). Dit is niet eens worst-case, dit ben je gewoon altijd kwijt (dus dit is zelfs slechter dan naïef). Algoritmiek: Divide & Conquer

8 Algoritmiek: Divide & Conquer
Snel herberekenen Herberekenen kan in 𝑂 1 door te schuiven 𝐻 0 =1⋅ ⋅ ⋅ ⋅ 20 1 𝐻 1 = ⋅ ⋅ ⋅ Algemeen: 𝐻 𝑖+1 =A i+m+1 + Σ (𝐻 𝑖 −𝐴 𝑖 Σ 𝑚−1 ) Rolling Hash Dit klopt inderdaad, want H(0) = en H(1) = H(0)*26 – 1*26^ = 25885*26 – 1*26^ = Algoritmiek: Divide & Conquer

9 Algoritmiek: Divide & Conquer
Rabin-Karp (2) RK2(A,P) ph = String2Int(P) sh = String2Int(A[1…m]) for i := 0 to n-m-1 if ph = sh yield i sh := sh × Σ + A[i+m] – A[i] × Σ m return Dit is een klein beetje beter, want dit gaat met O(n) arithmetische (optellen, vermenigvuldigen, etc…) operaties. We zeggen altijd dat dit soort operaties in O(1) gaan, dus dan zou Rabin-Karp O(n) zijn. Dit is alleen niet realistisch, want String2Int levert een getal op dat |Sigma|^m groot is. Zo’n getal heeft O(m) bits, en het is niet realistisch om te zeggen dat je daarop in O(1) kan werken. Een echte computer rekent met woorden van O(1) (in de praktijk 32, 64 of soms zelfs 128 of meer bits) en kan daarop in O(1) operaties doen. Dat is ook geen ideaal model, want als je wil gaan redeneren over het asymptotische gedrag van een algoritme moet je opeens rekening gaan houden met hoeveel tijd arithmetische operaties kosten (naarmate de getallen groter worden). We willen graag op de oude (met O(1) operaties op getallen) manier redeneren, zonder dat deze versie van Rabin-Karp O(n) wordt. Kan dat? JA! Een mogelijke aanname is het ‘word-RAM’-model. Hierin neem je aan dat de maximale grootte van een woord toeneemt naarmate het probleem groter wordt. Voor een probleem van grootte n mag je in O(1) operaties doen op woorden van w=O(log n) bits. Dit is een realistische aanname, want als je probleem groter is dan 2^w past een pointer naar de tekst van het probleem niet eens in één woord. Word-RAM sluit deze gekke Rabin-Karp variant uit, maar stelt ons toch in staat om ‘prettig’ te redeneren. Looptijd? Algoritmiek: Divide & Conquer

10 Algoritmiek: Divide & Conquer
Wat schiet dit op? Hash is 32-bit integer: werkt alleen voor 𝑚≤5 ( 26 7 > 2 32 ) Idee: bekijk getallen modulo 𝑞 Je kan alle tussenresultaten mod 𝑞 nemen, resultaat blijft gelijk Het is voor de analyse handig q priem te nemen, maar dat hoeft niet. Je kan ook q=2^32 kiezen; je hoeft dan niet expliciet de modulus te nemen, bij integer overflow krijg je ‘vanzelf’ de modulus cadeau. Algoritmiek: Divide & Conquer

11 Algoritmiek: Divide & Conquer
Voorbeeld 1⋅ ⋅26+7 ⋅26+15=25885≡10 (𝑚𝑜𝑑 23) 1≡1 (𝑚𝑜𝑑 23) 1⋅26+12=38≡15 (𝑚𝑜𝑑 23) 15⋅26+7=397≡6 (𝑚𝑜𝑑 23) 6⋅26+15=171≡10 (𝑚𝑜𝑑 23) Rekent nooit met getallen >𝑞 Σ Algoritmiek: Divide & Conquer

12 Algoritmiek: Divide & Conquer
Rabin-Karp (3) RK3(A,P) ph = String2Int(P) % q sh = String2Int(A[0…m-1]) % q for i := 0 to n-m-1 if ph = sh yield i sh := sh × Σ + A[i+m] – A[i] × Σ m % q return Nu gaat het écht in O(n) als q niet te groot is (d.w.z. in een machine-woord past zodat arithmatische operaties in O(1) gaan). Algoritmiek: Divide & Conquer

13 Algoritmiek: Divide & Conquer
Spurious Hits Zoek orit in algoritme; orit =15⋅ ⋅ ⋅ =276062≡16 𝑚𝑜𝑑 17 algo = 1⋅ ⋅26+7 ⋅26+15=25885≡11 𝑚𝑜𝑑 17 lgor = 12⋅26 +7 ⋅26+15 ⋅26+18=216052≡16 𝑚𝑜𝑑 17 Orit en lgor hebben dezelfde rest modulo 17 maar zijn niet dezelfde string. Rabin-Karp kan dus false positives geven. Algoritmiek: Divide & Conquer

14 Algoritmiek: Divide & Conquer
Rabin-Karp (4) RK4(A,P) ph = String2Int(P) % q sh = String2Int(A[0…m-1]) % q for i := 0 to n-m-1 if ph = sh && CheckMatch(A,i,P) yield i sh := sh × Σ + A[i+m] – A[i] × Σ m % q return Dit gaat nog steeds in O(n), maar we betalen nu O(m) extra voor iedere (al dan niet spurious) hit. Algoritmiek: Divide & Conquer

15 Algoritmiek: Divide & Conquer
Looptijd O(n) voor het checken van de hash, PLUS: O(m) per gevonden substring O(m) per spurious hit Wat is het aantal spurious hits? Het is acceptabel dat echte hits wat meer tijd kosten, maar (veel) spurious hits hebben we liever niet. Algoritmiek: Divide & Conquer

16 Algoritmiek: Divide & Conquer
Gemiddelde looptijd Aaname: 𝑞 vast, tekst vast, patroon random Kans dat het patroon zelfde hash heeft als een gegeven shift van de tekst is 1/𝑞 𝐸 𝑠𝑝𝑢𝑟𝑖𝑜𝑢𝑠 ℎ𝑖𝑡𝑠 =𝐸 Σ 𝑖 𝐼 𝑠ℎ𝑖𝑓𝑡 𝑖 𝑖𝑠 ℎ𝑖𝑡 = Σ 𝑖 𝐸 𝐼 𝑠ℎ𝑖𝑓𝑡 𝑖 𝑖𝑠 ℎ𝑖𝑡 = Σ 𝑖 1/𝑞=𝑛/𝑞 Gemiddelde looptijd: 𝑂 𝑛+𝑛𝑚/𝑞 Als Q ongeveer m is is dit O(n). Zo’n Q kun je krijgen met log(m) bits, dus in het RAM-model krijg je écht een (gemiddeld) O(n)-algoritme. Algoritmiek: Divide & Conquer

17 Algoritmiek: Divide & Conquer
Aanvallen! Er zijn superslechte invoeren te bedenken Tekst: aaaaaaaaaaaaaaa, patroon: aaaaaaXX Een aanvaller kan ons systeem platleggen met slechte invoer (DOS-aanval) Oplossing: maak 𝑞 random (priem) De tekst heeft altijd dezelfde hash (want alle substrings zijn hetzelfde) dus als ik XX zo kies dat aaaaaaXX ook die hash heeft (dit kan met een niet al te lange XX, O(log q) karakters lang) heb ik steeds een spurious hit die ook nog lang kost om te checken (want de prefix aaaaaa matcht vaak). Algoritmiek: Divide & Conquer

18 Algoritmiek: Divide & Conquer
Verwachte looptijd Gemiddeld v.s. Verwacht Bekijk vaste strings en shift orit =15⋅ ⋅ ⋅ =276062 lgor =12⋅ ⋅ ⋅ =216052 Wanneer spurious hit? Gemiddeld: je bekijkt alle mogelijke invoeren, doet uitspraak over looptijd gemiddeld over die invoeren (vgl. quicksort gemiddelde looptijd) Verwacht: je bekijkt één (worst-case) invoer, doet uitspraak over verwachte looptijd (vgl. randomized quicksort) Algoritmiek: Divide & Conquer

19 Algoritmiek: Divide & Conquer
Verwachte looptijd orit =15⋅ ⋅ ⋅ =276062 lgor =12⋅ ⋅ ⋅ =216052 𝐻 𝐴 𝑘…𝑘+𝑚 ≡𝐻 𝑃 𝑚𝑜𝑑 𝑞 ⇔𝐻 𝐴 𝑘…𝑘+𝑚 −𝐻 𝑃 ≡0 (𝑚𝑜𝑑 𝑞) 276062−216052=60010=2⋅5⋅17⋅353 Spurious hit als q een van de priemfactoren is Je bekijkt het verschil tussen de hash en het patroon (nog niet mod q gereduceerd). Er is een spurious hit als q dit verschil deelt. We nemen aan dat q random (wel priem) uit het bereik [Q, 2Q] wordt gekozen. Het aantal priemgetallen <n is ongeveer n / log n. Daarom hebben wij ongeveer 2Q / log 2Q – Q / log Q mogelijk te kiezen waarden voor q, dit zijn er (op constante factoren na) ongeveer Q / log Q (die allemaal met gelijke kans gekozen worden). Nu willen we graag weten: wat is de kans dat een bepaald verschil van de hashes, toevallig q als deler heeft. Omdat q>Q priem is, kun je q niet krijgen door priemdelers <Q samen te stellen. Daarom hoeven we ons alleen maar bezig te houden met priemdelers >= Q. De hashes zelf zijn hoogstens |Sigma|^m, dus het verschil is ook hoogstens zo groot. Zo’n getal heeft hoogstens log_Q (|Sigma|^m) relevante (dwz >=Q) priemdelers (waarom?). Er zijn n mogelijke shifts, dus in de worst case hebben we n log_Q(|Sigma|^m) priemdelers. Als je dit uitschrijft, krijg je nm log(|Sigma|) / log(Q). Als we verwacht hoogstens O(1) spurious hits willen, moet dit <= het aantal mogelijke priemen zijn, dus nm log(|Sigma|) / log(Q) <= Q / log Q. De / log Q valt tegen elkaar weg, dus dit geldt als nm log(|Sigma|) <= Q. Dan moet Q ongeveer log(nm log (|Sigma|)) bits hebben, d.w.z. w = log(n) + log(m) + log log(|Sigma|). Dat past perfect met het RAM-model, want de invoer is (n+m) log(|Sigma|) groot: je hebt n+m karakters die ieder log(|Sigma|) bits kosten om weer te geven. Een woord mag dus maximaal log(n+m) + log log(|Sigma|) bits hebben, en dit is (in grote O) <= log(n) + log(m) + log log(|Sigma|). Algoritmiek: Divide & Conquer

20 Algoritmiek: Divide & Conquer
Rabin-Karp Rolling hash met O(1) updates Practicum 3: Uitbreiding naar 2D… String matching in verwacht 𝑂 𝑛+𝑚𝑘+𝑚 𝑠𝑝. ℎ𝑖𝑡𝑠 =𝑂(𝑛+𝑚𝑘) tijd Nog wel 𝑂 𝑚 tijd per echte hit Veel hits = veel tijd Algoritmiek: Divide & Conquer

21 Matching met Eindige Automaat
Eindige Automaat/Finite Automaton 𝑂(𝑛) tijd met 𝑂 𝑚 Σ preprocessing Geen kosten voor hits Stel we zoeken in een patroon xxxxxxxxxAAAAYxxxx naar AAAAZ en bekijken de shift AAAAZ Na 4 gelijke A’s zien we dat de Y niet matcht met Z. Het naïve algoritme zou nu AAAZ 1 plek verplaatsen, en AAAYx Gaan vergelijken met AAAAZ. Maar omdat Y niet voorkomt in ons patroon, is de eerste shift die überhaupt kans maakt: xxxxxxxxxAAAAYxxxx AAAZ De automaat generaliseert dit. Algoritmiek: Divide & Conquer

22 Algoritmiek: Divide & Conquer
Eindige Automaat Toestanden: 𝑄 Overgangen: 𝛿:𝑄×Σ→𝑄 Algoritmiek: Divide & Conquer

23 Constructie (formeel)
𝑚+1 states: niks gematched, 1 gematched, …, 𝑛 gematched (alles) 𝛿 𝑖, 𝑥 = 𝑖+1 als 𝑥=𝑃[𝑖] de grootste 𝑗≤𝑖 zodat 𝑃 𝑖−𝑗…𝑖−1 =𝑃[0…𝑗−2] en 𝑃 𝑗−1 =𝑥 0 indien zo’n 𝑗 niet bestaat Het tweede geval is het belangrijkste, bij het implementeren kun je de overige cases afvangen met dit geval als je het iets anders formuleert. Algoritmiek: Divide & Conquer

24 Constructie (informeel)
We weten dat 𝑃[0…𝑖−1] het huidige stuk tekst matcht Toevoegen karakter 𝑥 Als 𝑥=𝑃[𝑖] matchen we één karakter meer Anders zoeken we de langste suffix van 𝑃 0…𝑖−1 0…𝑖−1 𝑥 die ook een prefix van 𝑃 is 𝑃=𝑎𝑏𝑏𝑎𝑏𝑎: als je na 𝑎𝑏𝑏𝑎𝑏 een 𝑏 ziet, is de langste suffix 𝑎𝑏𝑏𝑎𝑏𝑏 Voorbeeld met ANANAS zoeken in NANAANANANAS Algoritmiek: Divide & Conquer

25 Algoritmiek: Divide & Conquer
DFA Matcher DFA-Match(A,P) s := 0 δ := overgangsfunctie van P for i := 0 to n-1 s := δ(s, A[i]) if s = m yield i-m+1 return DFA = Deterministic Finite Automaton ([deterministische] eindige automaat) Algoritmiek: Divide & Conquer

26 Algoritmiek: Divide & Conquer
Bouwen van 𝛿 Kan in 𝑂 𝑚 3 Σ met naïef algoritme Kan ook in 𝑂 𝑚 Σ Finite Automata: je kan op iedere reguliere expressie matchen 𝑂(𝑛) tijd, 𝑂 𝑚 Σ preprocessing Worst case: 𝑂 𝑚 2 preprocessing én ruimte Je kan het net iets beter doen dan m|Sigma| als |Sigma| groot is: als je een karakter tegenkomt dat niet in P voorkomt, kun je dit in “overig” gooien. Maar m^2 is nog steeds te veel, het kan beter. Algoritmiek: Divide & Conquer

27 Algoritmiek: Divide & Conquer
Knuth-Morris-Pratt Verbetering van Eindige Automaat Ook in 𝑂 𝑛 tijd zoeken Maar met maar 𝑂 𝑚 preprocessing en ruimte Algoritmiek: Divide & Conquer

28 Algoritmiek: Divide & Conquer
Knuth-Morris-Pratt Automaat: indien 𝑥 een mismatch is, kijk welke (langste) suffix van 𝑃 0…𝑠−1 𝑥 een prefix van 𝑃 is KMP: kijk alleen naar 𝑃 0…𝑠−1 , niet naar 𝑥! In geval van mismatch bekijken we 𝑥 nog een keer Stel we zoeken in een patroon xxxxxxxxxABABYxxxx naar ABABZ en bekijken de shift ABABZ Na 4 gelijke A’s zien we dat de Y niet matcht met Z. De automaat wéét dat Y niet in ABABZ zit, en gaat gelijk verder met xxxxxxxxxABABYxxxx Om de preprocessing goedkoper te maken, kijkt KMP níet naar die Y. KMP weet wel dat ABAB kansloos is, want de eerste A uit het patroon matcht nooit met de B uit het patroon (die ook in de haystack-tekst zit). Algoritmiek: Divide & Conquer

29 Algoritmiek: Divide & Conquer
KMP Matcher KMP-Match(A,P) s := 0 π := prefixfunctie van P for i := 0 to n-1 if A[i] = P[s] s := s + 1 else if s > 0 s = π[s] i := i - 1 if s = m yield i-m+1 ; s = π[s] ; i := i - 1 return Met i := i – 1 bedoel ik dat i niet opgehoogd moet worden door de for-loop. Als A[i]=P[s] en s is niet uiteindelijk m, neemt i dus 1 toe, en anders blijft hij gelijk. Dit is O(n+m): je kan dit met amortiseren inzien, neem s als potentiaalfunctie. Iedere keer dat i toeneemt, neemt s ook toe. Als i niet toeneemt, neemt s af. s kan hoogstens i keer toenemen. NB: Amortiseren wordt pas later behandeld. Algoritmiek: Divide & Conquer

30 Algoritmiek: Divide & Conquer
Prefixfunctie 𝜋 𝑠 = grootste 𝑗<𝑠 zodat 𝑃 0…𝑗−1 =𝑃[𝑠−𝑗…𝑠−1] Opmerking 1: je kan deze 𝑗’s enumereren Geeft alle 𝑗’s die aan de eis voldoen while(j > 0) j = π[j] yield j Voorbeeld met ANANAS: pi[0] = Als we niks gematched hebben blijft niks gematched pi[1] = We zoeken de grootste (strikte) prefix van A die een suffix van A is, en dat is de lege string. pi[2] = De prefixes van AN zijn A en 0, de suffixes van AN zijn N en 0, de grootste gelijke is 0 pi[3] = Voor ANA hebben we prefixes AN, A, 0 en suffixes NA, A, 0, de grootste gelijke is A pi[4] = ANAN: prefixes ANA, AN, A en 0, suffixes NAN, AN, N en 0, grootste gelijke is AN pi[5] = ANANA: prefixes ANAN, ANA, AN, A en 0, suffixes NANA, ANA, AN en A, grootste gelijke is ANA pi[6] = ANANAS: iedere suffix eindigt op S, maar er is geen prefix die op S eindigt Gebruik ANANAS ook als voorbeeld voor opmerking 1. Algoritmiek: Divide & Conquer

31 Algoritmiek: Divide & Conquer
while(j > 0) j = π[j] yield j Prefixfunctie 𝜋 𝑠 = grootste 𝑗<𝑠 zodat 𝑃 0…𝑗−1 =𝑃[𝑠−1−𝑗…𝑠−1] Opmerking 1: je kan deze 𝑗’s enumereren Opmerking 2: splits de eis in twee delen 𝑃 0…𝑗−2 =𝑃 𝑠−2−𝑗…𝑠−2 𝑃 𝑗−1 =𝑃[𝑠−1] a) kun je met opmerking 1 bepalen! Algoritmiek: Divide & Conquer

32 Algoritmiek: Divide & Conquer
Prefixfunctie ComputePI(s) j := ComputePI(s – 1) while(j > 0 && P[j] ≠ P[s - 1]) j := ComputePI(j) if(P[j] ≠ P[s - 1]) return 0 return j + 1 Door s-1 te doen haal je de laatste letter weg, je kijkt nu naar wat de langste prefix die ook een suffix is van P[0 … s – 2]. De eerste die aan de eis voldoet is Pi[s – 2]. Als dan ook de letter na die gevonden prefix overeen komt met de letter die we graag er achter vast plakken (om een suffix van P[0 … s-1] te krijgen) klopt, hebben we hem. Anders kijken we naar de op één na grootste prefix die ook een suffix is van P[0 … s – 2] (door nog eens Pi te nemen). Dit geeft een DP-algoritme voor de prefixfunctie. Om in te zien dat dit snel is, helpt het om het iteratief om te schrijven. Dynamisch Programmeren! Algoritmiek: Divide & Conquer

33 Prefixfunctie (iteratief)
ComputePI(s) π[0] := 0 ; π[1] := 0 j := 0 for s := 2 to m – 1 while(j > 0 && P[j] ≠ P[s - 1]) j = π[j] if(P[j] = P[s - 1]) j := j + 1 π[s] = j Wederom kun je met amortiseren beredeneren dat dit O(m) is! j neemt hoogstens m keer toe, en dus ook hoogstens m keer af in de while. De reden dat we met de vorige j verder rekenen is de assignment j := ComputePI(s - 1) in de recursieve versie. Je gebruikt eigenlijk KMP binnen in ComputePI. Algoritmiek: Divide & Conquer

34 Algoritmiek: Divide & Conquer
Knuth-Morris-Pratt Matching in 𝑂(𝑛) met 𝑂 𝑚 preprocessing Eindige Automaat maar dan slimmer Automaat bekijkt elke letter 1 keer KMP vaker Algoritmiek: Divide & Conquer

35 Algoritmiek: Divide & Conquer
String Matching Naïef Rabin-Karp met Rolling Hash Eindige Automaat Knuth-Morris-Pratt Nog beter mogelijk: Boyer-Moore 𝑂(𝑛/𝑚) Boyer-Moore heeft in de best case complexiteit O(n/m). Idee: door te kijken naar het EIND van het pattern kun je detecteren dat een hele hoop shifts niet kunnen (bijvoorbeeld als je op de plek van het eind van het pattern in de haystack een karakter ziet die nergens in het pattern voorkomt, dan hoef je sommige karakters helemaal niet te bekijken. Je loopt (als je geluk hebt) met sprongen ter grootte van je pattern door de haystack heen! Algoritmiek: Divide & Conquer


Download ppt "String Matching Algoritmiek."

Verwante presentaties


Ads door Google