Přesné vyhledávání vzorku v textu

 

Nalezení pozice fragmentu textu (slova, sousloví, ...) v dokumentu, resp. potvrzení či vyvrácení jeho přítomnosti v něm, patří k základním algoritmům při implementaci DIS.

V průběhu vývoje algoritmů byla postupně navržena řada postupů, jak tento problém řešit.

 

A) Triviální algoritmus pro vyhledávání vzorku textu

 

Triviální algoritmus je pro implementaci nejjednodušší. Vzorek V se postupně přikládá k textu T od pozice 1, 2, 3, atd. Po každé, přiložení se ověřuje, zda se všechny znaky vzorku shodují s textem od dané pozice. Viz. přiložený zdrojový text v Pascalu.

 

const v = 'abc';

      t = 'abcdabceabababcabcabdbcd';

{          ^   ^       ^  ^            }

{          1   5       13 16           }

 

var i: byte;

 

function trivi(t,v: string; k:byte):byte;

 

{hledani vzorku V v textu T, od pozice K+1}

{pokud neni, vraci 0}

 

var m, n: byte;

    i, j: integer;

    nalezen: boolean;

 

begin

  m := length(t);

  n := length(v);

  i := k+1;

  nalezen := false;

  while not nalezen and (i <= m-n+1) do begin

    j := 1;

    while (j <= n) and (v[j] = t[i+j-1]) do

      inc(j);

    if (j > n)

      then nalezen := true

      else inc(i);

    end;

  if nalezen

    then trivi := i

    else trivi := 0;

  end;

 

begin

  i := 0;

  repeat

    i := trivi(t,v,i);

    if i > 0

      then writeln(i:3);

    until i = 0;

  end.

 

Nevýhodou algoritmu je velká časová složitost v průměrném případě. Pokud označíme délku textu T písmenem m a délku vzorku V písmenem n, je asymptotická složitost triviálního algoritmu o(m.n). Příkladem může být vyhledání vzorku anb v textu amb.

 

Pozn: V přirozených jazycích nedochází ke shodě počátků slov příliš často, a proto je průměrná asymptotická složitost algoritmu redukována na o(m.kL), kde kL je konstanta závislá na jazyku.

 

Jiné algoritmy se snaží docílit lepší asymptotické složitosti (v průměrném i nejhorším případě) než triviální algoritmus předzpracováním vzorku nebo textu samotného. Během předzpracování je prozkoumána struktura vzorku (textu) a na jejím základě vytvořen vyhledávací algoritmus (vyhledávací stroj), který již pracuje lineárním čase vzhledem k délce textu T.

 

Z hlediska předzpracování textu lze algoritmy rozdělit na čtyři kategorie:

 

 

Do kategorie I. patří triviální algoritmus. Většina používaných algoritmů pro hledání v textu patří do kategorie II., tzn. že předzpracovávají vyhledávaný vzorek a na základě výsledku hledají v původním textu.

 

Tyto algoritmy lze dále rozdělit podle počtu vzorků, které lze vyhledávat najednou, a směru, ve kterém jsou vzorky s textem porovnávány

 

 

B) Knuth-Morris-Prattův algoritmus

 

KMP algoritmus je založen na následující úvaze: časové ztráty triviálního algoritmu vznikají tím, že v případě neshody vzorku s textem se vzorek posune o jednu pozici doprava a znovu se celý kontroluje. Je nutné se tedy v textu T často vracet při porovnávání se vzorkem zpět k pozicím, které již byly zkontrolovány. To znehodnocuje informaci, která byla získána předchozími porovnáními znaků.

Snahou tohoto algoritmu je v případě neshody vzorku s textem vzorek posunout doprava tak, aby nebylo nutné se ve vlastním textu vracet. Musí tedy být zaručeno, že všechny pozice vzorku, které zůstaly pod již zkontrolovanou částí textu se s tímto textem shodují. viz obr.:

 

Aby bylo možné vorek posunout se zachováním informace z předchozího porovnávání, musí se (vlastní) prefix již porovnané části vzorku, který zůstane po posunu v již zkontrolované oblasti rovnat (vlastnímu) postfixu již zkontrolované části. Při posunech vzorku se nemůže žádná možnost shody s textem vynechat a vzorek se musí posunout co nejméně. To odpovídá výběru co nejdelšího vlastního prefixu.

 

Pokud se vzorek na pozici j neshoduje s textem a nejdelší vlastní prefix zkontrolované části vzorku, rovnající se postfixu zkontrolované části má délku k, zůstane po posunu k znaků vzorku před pozicí porovnávání a vzorek se začne porovnávat od pozice k+1. Za předpokladu, že opravné pozice pro chyby na různých pozicích j jsou uloženy v poli P (P[j] = oprava pro chybu na pozici j), bude algoritmus vypadat následovně:

 

type shift = array [ 1..255 ] of 0..254;    {posuny pro vyhl. stroj}

 

const v = 'abc';

      t = 'abcdabceabababcabcabdbcd';

{          ^   ^       ^  ^            }

{          1   5       13 16           }

 

var p: shift;

    i: byte;

 

function kmp(t,v: string; k:byte; var p: shift):byte;

 

{hledani vzorku V v textu T, od pozice K+1 za pomoci pole P}

{pokud neni, vraci 0}

 

var m, n: byte;

    i, j: integer;

    nalezen: boolean;

 

begin

  m := length(t);

  n := length(v);

  i := k+1;

  j := 1;

  while (i <= m) and (j <= n) do begin

    while (j > 0) and (v[j] <> t[i]) do

      j := p[j];

    inc(i);

    inc(j);

    end;

  if (j > n)

    then kmp := i-j+1

    else kmp := 0;

  end;

 

begin

  pocitej_shift(v,p);

  i := 0;

  repeat

    i := kmp(t,v,i,p);

    if i > 0

      then writeln(i:3);

    until i = 0;

  end.

Opravy P[j] pro pozice j lze určit následujícím postupem:

 

P[1] = 0

 

Pokud známe opravy pro pozice 1 .. j-1, je snadné spočítat opravu pro pozici j.

 

P[j-1] obsahuje opravu pro pozici j. Tzn, že P[j-1]-1 znaků ze začátku vzorku se shoduje se stejným počtem znaků před j-1 ní pozicí ve vzorku.

 

 

Pokud se také j-1 ní pozice vzorku shoduje s P[j-1] ní pozicí, může se prefix prodloužit a tedy správná hodnota pro P[j] je o jedna vyšší než předchozí.

 

 

Pokud se j-1 ní a P[j-1] ní pozice vzorku neshodují,

 

 

 

znamenalo by to, že pokud by se oprava určila stejně jako v předchozím případě, došlo by po chybě na pozici j a následném posunu vzorku k situaci, že nebude souhlasit znak před pozicí na které se bude pokračovat v porovnávání. Pro chybu na této předchozí pozici je však již známa oprava  (čísla P[1] .. P[j-1] jsou již spočtena) a je tedy nutné posunout vzorek více (stanovit nižší p[j]) tak aby došlo k souhlasu i na této pozici. Je nutné tedy sledovat opravy počínaje j-1 ní pozicí tak dlouho, dokud se j-1 ní pozice vzorku neshoduje s pozicí, na kterou odkaz směřuje.

 

 

Výsledný algoritmus pro výpočet posunů tedy vypadá následovně:

 

type shift = array [ 1..255 ] of 0..254;    {posuny pro vyhl. stroj}

 

procedure pocitej_shift(v: string; var p: shift);

 

{ vypocte hodnoty posunu vzurku pro KMP algoritmus }

 

var n: byte;

    j, k, l: byte;

 

begin

  n := length(v);

  p[1] := 0;

  j := 2;

  while (j <= n) do begin

    k := j-1;

    l := k;

    repeat

      l := p[l];

      until (l = 0) or (v[l] = v[k]);

    p[j] := l+1;

    inc(j);

    end;

  end;

 

Časová složitost algoritmu je o(m+n). Při znalosti P se v textu nikdy nevrací. Při posunutí vzorku sice znovu porovnává stejnou pozici v textu podruhé, ale posunů vzorku nemůže být více než o(m). Porovnávání samotné tedy pracuje v čase o(m).

Stejně tak časová složitost předzpracování je o(n).


C) Aho-Corasickové algoritmus

 

KMP algoritmus je určen pro vyhledávání jednoho vzorku v textu. Pokud chceme vyhledávat více vzorků najednou, a to je při tvorbě DIS spíše pravidlem než výjimkou, bylo by nutné spouštět KMP algoritmus vícakrát po sobě. AC algoritmus je navržen tak, aby mohl vyhledávat více vzorků (konečný počet) zároveň během jediného průchodu textem. Opět se jedná o algoritmus s předzpracováním textu.

 

Vyhledávání vzorků V1, V2, ... Vk je založené na vyhledávacím stroji

 

S = ( Q, X, q0, g, f, F )

 

kde

 

Q je množina stavů

X je abeceda

q0 Î Q je počáteční stav

g: Q x X ® Q je dopředná funkce (go)

f: Q ® je zpětná funkce (fail)

F Í Q je množina koncových stavů.

 

Při vyhledávání stroj simuluje přechodovou funkci d konečného automatu následujícím způsobem:

 

d(q,x) = g(q,x)          ,pokud je g(q,x) definována

 (q,x) = d (f(q),x)      ,jinak

 

definice je korektní, protože vzdálenost f(q) od počátečního stavu je menší než vzdálenost stavu q.

 

Každý stav vyhledávacího stroje reprezentuje jeden prefix některého (některých) z vyhledávaných vzorků. Stav q0 reprezentuje prázdný prefix l.

Pokud stav q reprezentuje prefix p a stav q’ reprezentuje prefix px pro x Î X, potom je pro dvojici q, x definována funkce g(q,x) = q’.

Navíc je pro všechan x Î X která nejsou prefixem žádného ze vzorků definována funkce g(q0,x) = q0.

 

Příklad: pro vyhledávání vzorků „he“, „her“ a „she“ vypadá funkce g následovně.

 

 

Funkce f (fail) je definována tak že z každého stavu vede do stavu, který reprezentuje nejdelší vlastní příponu řetězce, který on sám reprezentuje. Tedy  například ze stavu reprezentujícího „sh“ vede do stavu „h“, nebo ze stavu „h“ do stavu „“. Zpětná funkce se určuje postupně podle vzrůstající vzdálenosti od počátečního vrcholu.

 

f(q) = q0 pro q ve vzdálenosti 1.

f(g(q’,x)) = d(f(q’),x) pro q = g(q’,x) ve větší vzdálenosti. Tedy pokud chceme zjistit f(q) pro vrchol, do kterého se přechází ze stavu q’ pod znakem x, přejdeme ze stavu q’ podle zpětné funkce (je již definována, q’ je blíž k začátku) a poté uděláme krok vpřed podle přechodové funkce d podle znaku x.

 

Výsledná definice f pro výše uvedený příklad je následující:

 

 

Pokud se vyhledává poze jeden vzorek, je výsledný automat shodný s KMP algoritmem. Časová složitost algoritmu je

 

kde m je délka textu a ni délka vzorku Vi.

 

Detekce vzorků:

Při konstrukci AC vyhledávacího stroje chceme, aby automat detekoval při své simulaci i textové vzorky, které jsou obsaženy v jiných vzorků (v uvedeném příkladě je ‘he’ obsažen v ‘she’, ale je obsažen i ve ‘wheel’). K tomu je nutné buď při konstrukci automatu předem vytvořit pro každý uzel automatu seznam v něm detekovaných vzorků (v příkladě stav „she“ detekuje vzorek ‘she’ i ‘he’), nebo tento seznam opakovaně vytvářet v době simulace konečného automatu při vstupu do uzlu. Seznam detekovaných vzorků pro daný stav vyhl. stroje získáme tak, že se projdou všechny stavy z daného stavu do počátečního stavu podél zpětné funkce. Pro stav „she“ v příkladu se do seznamu vloží vzorky detekované stavy „she“, „he“ a „“.

 

Simulaci konečného automatu vyhledávacím strojem ukazuje následující zdrojový text v jazyce Pascal.

 

const maxstav = 100;

      maxvzorku = 10;

      fail = -1;

      t = 'NEJAKY TEXT PRO PROHLEDAVANI';

 

type stavy = 0..maxstav;

     fstavy = -1..maxstav;

     vzorky = array [1..maxvzorku] of string;

     abeceda = ' '.. #$7F;

     gofunc = array [stavy,abeceda] of fstavy;

     failfunc = array [stavy] of stavy;

 

procedure AC(t: string; g: gofunc; f: failfunc);

{vyhledani mnoziny vzorku v textu t}

{vstup: t ... text}

{       g ... dopredna fce}

{       f ... zpetna fce  }

 

var q: stavy;   {aktualni stav automatu}

    i: byte;    {pozice v textu}

 

function delta(q:stavy; x: abeceda):stavy;

 

begin

  while g[q,x] = fail do

    q := f[q];

  delta := g[q,x];

  end;

 

begin

  q := 0;

  for i := 1 to length(t) do begin

    q := delta(q,t[i]);

    vypis(q);                   {vypis vzorku koncicich znakem t[i]}

    end;

  end;

 

begin

  k := 0;

  postavautomat(v,g,f);   {neni implementovana !!!}

  ac(t,g,f);

  end.