portaldacalheta.pt
  • Основен
  • Пъргав
  • Иновация
  • Тенденции
  • Back-End
Наука За Данни И Бази Данни

Покорете търсенето на струни с алгоритъма Aho-Corasick



Манипулирането на низове и намирането на модели в тях са основни задачи в науката за данни и типична задача за всеки програмист.

Ефективните низови алгоритми играят важна роля в много процеси на науката за данни. Те често правят тези процеси достатъчно осъществими за практическа употреба.



Алгоритъм на Aho-Corasick за ефективни проблеми с търсенето на низове

В тази статия ще научите за един от най-мощните алгоритми за намиране на модели в голямо количество текст: алгоритъмът Aho-Corasick. Този алгоритъм използва a trie структура на данните (произнася се „опитайте“), за да следите шаблоните за търсене и използва прост метод за ефективно намиране на всички случаи на даден набор от шаблони във всеки текстов балон.



По-ранна статия в ApeeScape Engineering Blog демонстрира алгоритъм за търсене на низове за същия проблем. Подходът, възприет в тази статия, предлага по-добра изчислителна сложност.

Алгоритъмът Knuth-Morris-Pratt (KMP)

За да разберем как можем ефективно да търсим множество шаблони в текст, първо трябва да се справим с по-лесен проблем: намиране на един шаблон в даден текст.



Да предположим, че имаме голяма петна с дължина н и модел (който искаме да намерим в текста) с дължина М . Ако искаме да търсим еднократно появяване на този модел или всички случаи, можем да постигнем изчислителна сложност на O (N + M) използвайки KMP алгоритъма.

Префикс функция

Алгоритъмът KMP работи чрез изчисляване на префикс функция на шаблона, който търсим. Префиксната функция изчислява предварително алтернативна позиция за всеки префикс в шаблона.



Нека дефинираме нашия модел за търсене като низ, обозначен с S. За всеки подниз S [0..i], където i> = 1, ще намерим максималния префикс на този низ, който е и суфиксът на този низ. Ще отбележим дължината на този префикс P [i].

За модела „абракадабра“ префиксната функция ще генерира следните основни позиции:



Индекс (i) 0 един 2 3 4 5 6 7 8 9 10
Характер да се б r да се ° С да се д да се б r да се
Дължина на префикса (P[i]) 0 0 0 един 0 един 0 един 2 3 4

Префиксната функция идентифицира интересна характеристика на шаблона.

Да вземем конкретен префикс на шаблона като пример: 'abracadab'. Стойността на функцията за префикс за този префикс е две. Това показва, че за този префикс „abracadab“ има суфикс с дължина две, който точно съвпада с префикса с дължина две (т.е. моделът започва с „ab“, а префиксът завършва с „ab“). Също така, това е най-дългото съвпадение за този префикс.



Изпълнение

Ето една функция C #, която може да се използва за изчисляване на префиксната функция за всеки низ:

public int[] CalcPrefixFunction(String s) { int[] result = new int[s.Length]; // matriz con valores de función de prefijo result[0] = 0; // la función de prefijo siempre es cero para el primer símbolo (su caso degenerado) int k = 0; // valor actual de la función de prefijo para (int i = 1; i 0 && s[i] != s[k]) k = result[k - 1]; if (s[k] == s[i]) k++; // hemos encontrado el prefijo más largo - caso 1 result[i] = k; // almacenar este resultado en la matriz } resultado de devolución; }

Изпълнявайки тази функция в малко по-дълъг модел, 'abcdabcabcdabcdab' създава това:



Индекс (i) 0 един 2 3 4 5 6 7 8 9 10 единадесет 12 13 14. петнадесет 16.
Характер да се б ° С д да се б ° С да се б ° С д да се б ° С д да се б
Префикс Функция (P[i]) 0 0 0 0 един 2 3 един 2 3 4 5 6 7 4 5 6

Сложност на изчисленията

Въпреки че има два вложени цикъла, сложността на функцията за префикс е просто O (M) , където М е дължината на шаблона С .

Това може лесно да се обясни, като се разгледа как работят циклите.

Всички повторения на външния цикъл през i Те могат да бъдат разделени на три случая:

  1. Увеличение k в един. Цикълът завършва една итерация.

  2. Не променя нулевата стойност на k. Цикълът завършва една итерация.

  3. Това не променя или намалява положителна стойност от k.

Първите два случая могат да бъдат изпълнени най-много М пъти.

кой от следните принципи не е важен принцип, който трябва да се вземе предвид при персонализиране на система

За третия случай нека дефинираме P (s, i) = k1 и P (s, i + 1) = k2, k2 <= k1. Всеки от тези случаи трябва да бъде предшестван от случаите k1 - k2 от първия случай. Броят на намаленията не надвишава k1 - k2 + 1. И общо имаме не повече от 2 * М итерации.

Обяснение на втория пример

Нека да разгледаме втория примерен модел 'abcdabcabcdabcdab'. Ето как префиксната функция го обработва, стъпка по стъпка:

  1. За празен подниз и подниз „а“ с дължина едно стойността на функцията за префикс е зададена на нула. (k = 0)

  2. Погледнете подниза 'ab'. Текущата стойност на k е нула и знакът 'b' не е равен на символа 'a'. Тук имаме втория случай от предишния раздел. Стойността на k остава нула и стойността на префиксната функция за подниза 'ab' също е нула.

  3. Същото е и за поднизовете 'abc' и 'abcd'. Няма префикси, които са и суфиксите на тези поднизове. Стойността за тях остава нула.

  4. Сега нека разгледаме един интересен случай, подниза 'abcda'. Текущата стойност на k все още е нула, но последният знак от нашия подниз съвпада с първия си знак. Това задейства условието на s [k] == s [i], където k == 0 и i == 4. Масивът има индекс нула и k е индексът на следващия знак към префикса за максимална дължина. Това означава, че сме намерили префикса за максимална дължина за нашия подниз, който също е суфикс. Имаме първия случай, когато новата стойност на k е едно и следователно стойността на функцията за префикс P ('abcda') Едно е.

  5. Същият случай се среща и за следните два подниза, P („abcdab“) = 2 Y. P („abcdabc“) = 3 . Тук търсим нашия модел в текста, сравнявайки низове знак по знак. Да кажем, че първите седем знака от шаблона съвпадат с около седем последователни знака от обработен текст, но осмият знак не съвпада. Какво трябва да се случи след това? В случай на наивно съвпадение на низове, трябва да върнем седем знака и да започнем процеса на сравнение отново от първия символ на нашия шаблон. Със стойността на префиксната функция (тук P („abcdabc“) = 3 ) знаем, че суфиксът ни с три знака вече съвпада с три знака от текста. И ако следващият символ в текста е 'd', дължината на съответстващия подниз на нашия модел и подниз в текста се увеличава до четири ('abcd'). Напротив, P („abc“) = 0 и ще започнем процеса на сравнение от първия символ на шаблона. Но важното е, че не се връщаме по време на обработката на текста.

  6. Следващият подниз е 'abcdabca'. В подниза по-горе префиксната функция е равна на три. Това означава, че k = 3 е по-голямо от нула и в същото време имаме несъответствие между следващия символ в префикса (s [k] = s [3] = 'd') и следващия знак в суфикса (s [i] = s [7] ='a'). Това означава, че активираме условието на s [k]! =S [i] и че префиксът 'abcd' не може да бъде суфиксът на нашия низ. Трябва да намалим стойността на k и вземете префикса по-горе, за да сравните, когато е възможно. Както описахме по-рано, масивът има индекс нула и k е индексът на следващия знак, който проверяваме от префикса. Последният индекс на съответстващия в момента префикс е k - 1. Вземаме стойността на функцията за префикс за съответстващия в момента префикс k = resultado [k - 1] В нашия случай (третият случай) дължината на максималния префикс ще бъде намалена до нула и след това в следващия ред тя ще бъде увеличена до един, тъй като „а“ е максималният префикс, който е и суфиксът на нашия подниз.

  7. (Тук продължаваме процеса на изчисление, докато стигнем до по-интересен случай.)

  8. Започваме да обработваме следния подниз: 'abcdabcabcdabcd'. Текущата стойност на k е седем. Както при „abcdabca“ по-горе, и тук имаме несъвпадение: Тъй като знакът „a“ (седмият знак) не е равен на символа „d“, поднизът „abcdabca“ не може да бъде суфиксът на нашия низ. Сега получаваме вече изчислената стойност от функцията за префикс за 'abcdabc' (три) и сега имаме съвпадение: префиксът 'abcd' е и суфиксът на нашия низ. Максималният му префикс и стойността на функцията за префикс за нашия подниз са четири, защото това е текущата стойност на k

  9. Продължаваме този процес до края на модела.

Накратко: и двата цикъла отнемат не повече от 3M итерации, което показва, че сложността е O (M). Използването на паметта също е O (M).

Внедряване на алгоритъма на KMP

public int KMP(String text, String s) { int[] p = CalcPrefixFunction(s); // Calcular la función de prefijo para una cadena de patrón // La idea es la misma que en la función de prefijo descrita anteriormente, pero ahora // estamos comparando prefijos de texto y patrón. // El valor del prefijo de longitud máxima de la cadena del patrón que se encontró // en el texto: int maxPrefixLength = 0; for (int i = 0; i 0 && text[i] != s[maxPrefixLength]) maxPrefixLength = p[maxPrefixLength - 1]; // Si ocurrió una coincidencia, aumenta la longitud de la longitud máxima // prefijo. if (s[maxPrefixLength] == text[i]) maxPrefixLength++; // Si la longitud del prefijo tiene la misma longitud que la cadena del patrón, // significa que hemos encontrado una subcadena coincidente en el texto. if (maxPrefixLength == s.Length) { // Podemos devolver este valor o realizar esta operación. int idx = i - s.Length + 1; // Obtenga el prefijo de longitud máxima anterior y continúe la búsqueda. maxPrefixLength = p[maxPrefixLength - 1]; } } return -1; }

Горният алгоритъм прелиства текста, символ по знак и се опитва да увеличи максималния префикс както за нашия шаблон, така и за последователност от последователни знаци в текста. В случай на неуспех, ние няма да се върнем към предишната си позиция в текста. Знаем максималния префикс на намерения подниз на шаблона; този префикс е и суфиксът на този намерен подниз и можем просто да продължим търсенето.

Сложността на тази функция е същата като на префиксната функция, което прави цялостната изчислителна сложност O (N + M) с O (M) памет.

Любопитни факти: String.IndexOf () и String.Contains () в .NET рамката те имат алгоритъм със същата сложност под капака.

Алгоритъмът на Aho-Corasick

Сега искаме да направим същото за множество модели.

Да предположим, че има модели М на дължини L1 , L2 , ..., Lm . Трябва да намерим всички съвпадения на шаблони от речник в текст с дължина н .

Тривиално решение би било да се вземе всеки алгоритъм от първата част и да се стартира ** M ** пъти. Ние имаме сложност на O (N + L1 + N + L2 +… + N + Lm) , тоест (M * N + L) .

Всеки достатъчно сериозен тест убива този алгоритъм.

Вземането на речник с 1000 най-често срещани английски думи като модели и използването му за търсене на английската версия на „Войната и мира” на Толстой ще отнеме доста време. Книгата има повече от три милиона знака.

Ако вземем 10 000 най-често срещани думи на английски, алгоритъмът ще работи около 10 пъти по-бавно. Очевидно при входове, по-големи от това, времето за изпълнение също ще нарасне.

Тук алгоритъмът на Aho-Corasick работи с магията си.

Сложността на алгоритъма на Aho-Corasick е O (N + L + Z) , където С е броят на мачовете. Този алгоритъм е измислен от Алфред В. Ахо Y. Маргарет Дж. Корасик през 1975г.

Изпълнение

Тук се нуждаем от трие и добавяме към нашия алгоритъм идея, подобна на описаните по-горе функции за префикс. Ще изчислим стойностите на префиксните функции за целия речник.

Всеки връх в трие ще съхранява следната информация:

public class Vertex { public Vertex() { Children = new Hashtable(); Leaf = false; Parent = -1; SuffixLink = -1; WordID = -1; EndWordLink= -1; } // Enlaces a los vértices secundarios en el trie: // Clave: Un solo caracter // Valor: El ID del vértice public Hashtable Children; // Indica que una palabra del diccionario termina en este vértice public bool Leaf; // Enlace al vértice padre public int Parent; // Char que nos mueve desde el vértice padre al vértice actual public char ParentChar; // Enlace de sufijo del vértice actual (el equivalente de P [i] del algoritmo KMP) public int SuffixLink; // Enlace al vértice de hoja de la palabra de longitud máxima que podemos hacer con el prefijo actual public int EndWordLink; // Si el vértice es la hoja, guardamos el ID de la palabra public int WordID; }

Има няколко начина за прилагане на вторични връзки. Алгоритъмът ще има сложност от O (N + L + Z) в случай на масив, но това ще има допълнително изискване за памет от O (L * q) , където q е дължината на азбуката, тъй като това е максималният брой деца, които един възел може да има.

Ако използваме някаква структура с O (log (q)) достъп до неговите елементи, имаме допълнително изискване за памет от O (L) , но сложността на пълния алгоритъм ще бъде O ((N + L) * log (q) + Z) .

В случай на хеш таблица, имаме O (L) допълнителна памет и сложността на целия алгоритъм ще бъде O (N + L + Z) .

Този урок използва хеш таблица, защото ще работи и с различни набори от символи, например китайски символи.

Вече имаме структура за връх. След това ще дефинираме списък с върхове и ще стартираме основния възел на трие.

public class Aho { List Trie; List WordsLength; int size = 0; int root = 0; public Aho() { Trie = new List(); WordsLength = new List(); Init(); } private void Init() { Trie.Add(new Vertex()) size++; } }

След това добавяме всички шарки към трие. За това се нуждаем от метод за добавяне на думи към трие:

public void AddString(String s, int wordID) { int curVertex = root; for (int i = 0; i

На този етап всички думи на шаблона са в структурата на данните. Това изисква допълнителна памет на O (L) .

След това трябва да изчислим всички суфиксни връзки и връзки за въвеждане на речник.

За да стане ясно и лесно за разбиране, ще премина през нашето трие от корен до листа и ще направя изчисления, подобни на това, което направихме за алгоритъма KMP, но за разлика от алгоритъма KMP, където намираме максималната дължина на префикса, която също беше суфикса на същия подниз, сега ще намерим суфикса с максимална дължина на текущия подниз, който е и префиксът на някакъв модел в трие. Стойността на тази функция няма да бъде дължината на намерения суфикс; ще бъде връзката към последния знак от максималния суфикс на текущия подниз. Това имам предвид под връзката суфикс на връх.

Ще обработвам върхове по нива. За това ще използвам алгоритъм търсене по ширина (BFS) :

Трие, което се обработва от алгоритъм за амплитудно търсене

И по-долу е изпълнението на този кросоувър:

public void PrepareAho() { Queue vertexQueue = new Queue(); vertexQueue.Enqueue(root); while (vertexQueue.Count > 0) { int curVertex = vertexQueue.Dequeue(); CalcSuffLink(curVertex); foreach (char key in Trie[curVertex].Children.Keys) { vertexQueue.Enqueue((int)Trie[curVertex].Children[key]); } } }

И по-долу е методът CalcSuffLink за да се изчисли обвързването на суфикса за всеки връх (т.е. стойността на функцията на префикса за всеки подниз в трие):

public void CalcSuffLink(int vertex) { // Processing root (empty string) if (vertex == root) { Trie[vertex].SuffixLink = root; Trie[vertex].EndWordLink = root; return; } // Procesamiento de hijos de la raíz (subcadenas de un caracter) if (Trie[vertex].Parent == root) { Trie[vertex].SuffixLink = root; if (Trie[vertex].Leaf) Trie[vertex].EndWordLink = vertex; else Trie[vertex].EndWordLink = Trie[Trie[vertex].SuffixLink].EndWordLink; return; } // Los casos anteriores son casos degenerados en cuanto al cálculo de la función del prefijo; // el valor siempre es 0 y los enlaces al vértice raíz. // Para calcular el sufijo link para el vértice actual, necesitamos el sufijo // enlace para el padre del vértice y el personaje que nos movió a la // vértice actual. int curBetterVertex = Trie[Trie[vertex].Parent].SuffixLink; char chVertex = Trie[vertex].ParentChar; // Desde este vértice y su subcadena comenzaremos a buscar el máximo // prefijo para el vértice actual y su subcadena. while (true) { // Si hay una ventaja con el carácter necesario, actualizamos nuestro enlace de sufijo // y abandonar el ciclo if (Trie[curBetterVertex].Children.ContainsKey(chVertex)) { Trie[vertex].SuffixLink = (int)Trie[curBetterVertex].Children[chVertex]; break; } // De lo contrario, estamos saltando por enlaces de sufijo hasta que lleguemos a la raíz // (equivalente a k == 0 en el cálculo de la función de prefijo) o encontramos un // mejor prefijo para la subserie actual. if (curBetterVertex == root) { Trie[vertex].SuffixLink = root; break; } curBetterVertex = Trie[curBetterVertex].SuffixLink; // Go back by sufflink } // Cuando completamos el cálculo del enlace de sufijo para el actual // vertex, debemos actualizar el enlace al final de la palabra de longitud máxima // que se puede producir a partir de la subcadena actual. if (Trie[vertex].Leaf) Trie[vertex].EndWordLink = vertex; else Trie[vertex].EndWordLink = Trie[Trie[vertex].SuffixLink].EndWordLink; }

Сложността на този метод е ** O (L) **; в зависимост от изпълнението на детската колекция сложността може да бъде ** O (L * log (q)) **.

Тестът за сложност е подобен на теста на функцията за префикс на сложността в алгоритъма KMP.

Нека видим следното изображение. Това е показване на трие за речника {abba, cab, baba, caab, ac, abac, bac} с изчислена цялата ви информация:

Трие за речника, състоящо се от abba, cab, baba, caab, ac, abac и bac

Trie границите са тъмно сини, суфиксните връзки са светло сини, а речниковите суфикси са зелени. Възлите, съответстващи на записите в речника, са маркирани в синьо.

И сега ни трябва само още един метод: обработка на блок текст, който възнамеряваме да търсим:

public int ProcessString(String text) { // Estado actual del valor int currentState = root; // Valor de resultado apuntado int result = 0; for (int j = 0; j

И сега това е готово за използване:

Във входа имаме списък с модели:

List patterns;

И потърсете текст:

string text;

И тук как да залепите всичко заедно:

// Inicia la estructura trie. Como parámetro opcional podemos poner el aproximado // tamaño del trie para asignar memoria solo una vez para todos los nodos. Aho ahoAlg = new Aho(); for (int i = 0; i

И това е! Сега знаете как работи този прост, но мощен алгоритъм!

Aho-Corasick е наистина гъвкав. Моделите за търсене не трябва да бъдат само думи, но можем да използваме цели изречения или произволни низове.

производителност

Алгоритъмът е тестван на Intel Core i7-4702MQ.

За тестовете взех два речника: 1000-те най-често срещани думи на английски и 10 000-често срещаните думи на английски.

За да добавите всички тези думи към речника и да подготвите структурата на данните за работа с всеки от речниците, алгоритъмът изисква съответно 55 ms и 135 ms.

Алгоритъмът обработва истински книги с дължина 3-4 милиона знака в рамките на 1,0-1,3 секунди, докато за книга с около 30 милиона знака отнема 9,6 секунди.

Паралелизирайте алгоритъма на Aho-Corasick

Да вървите паралелно с алгоритъма на Aho-Corasick изобщо не е проблем:

Алгоритъмът на Aho-Corasick работи паралелно на четири части от даден текст.

Голям текст може да бъде разделен на множество парчета и множество нишки могат да бъдат използвани за обработка на всеки парче. Всяка нишка има достъп до генерираното трие въз основа на речника.

Ами думите, които са разделени на границата между фрагментите? Този проблем може лесно да бъде решен.

Позволявам н да бъде дължината на нашия голям текст, С е размерът на фрагмент и L да бъде дължината на най-големия шаблон в речника.

Сега можем да използваме прост трик. Разделяме парчетата с припокриване в края, например като [S * (i - 1), S * i + L - 1], където i е индексът на парчето. Когато получим съвпадение на шаблон, можем лесно да получим началния индекс на текущото съвпадение и просто да проверим дали този индекс е в диапазона на парчета без припокривания, [S * (i - 1), S * i - 1]

Универсален алгоритъм за търсене на низове

Алгоритъмът Aho-Corasick е мощен алгоритъм за комбиниране на низове, който предлага най-добрата сложност за всеки вход и не изисква много допълнителна памет.

Алгоритъмът често се използва в различни системи като проверки на правописа, филтри за нежелана поща, търсачки, биоинформатика / търсене на ДНК последователност и т.н. Всъщност някои популярни инструменти, които можете да използвате всеки ден, използват този алгоритъм зад кулисите.

Префиксната функция на самия алгоритъм на KMP е интересен инструмент, който намалява сложността на съвпадението на единичен модел до линейно време. Алгоритъмът Aho-Corasick следва подобен подход и използва трие структура на данни, за да направи същото за множество модели.

Надявам се, че този урок за алгоритъма на Aho-Corasick е полезен.

Директор на комуникациите

Други

Директор на комуникациите
Опростено балансиране на натоварването NGINX с Loadcat

Опростено балансиране на натоварването NGINX с Loadcat

Back-End

Популярни Публикации
ApeeScape разраства връзката си с Amazon Web Services, за да продължи да стимулира икономиката на талантите
ApeeScape разраства връзката си с Amazon Web Services, за да продължи да стимулира икономиката на талантите
Въведение в теорията и сложността на изчислимостта
Въведение в теорията и сложността на изчислимостта
Ръководство стъпка по стъпка за проектиране на персонализирани илюстрации без предишен опит
Ръководство стъпка по стъпка за проектиране на персонализирани илюстрации без предишен опит
Обяснено оптимизиране на ефективността на Magento
Обяснено оптимизиране на ефективността на Magento
Изчерпателно ръководство за дизайн на известия
Изчерпателно ръководство за дизайн на известия
 
Малки данни, големи възможности
Малки данни, големи възможности
Достъпност в мрежата: Защо стандартите W3C често се игнорират
Достъпност в мрежата: Защо стандартите W3C често се игнорират
Бъдещето на UX е нашето човечество
Бъдещето на UX е нашето човечество
Предвиждащ дизайн: Как да създадем магически потребителски опит
Предвиждащ дизайн: Как да създадем магически потребителски опит
Въведение в Python Microservices с Nameko
Въведение в Python Microservices с Nameko
Популярни Публикации
  • какво е пом в селен
  • как се прави 3d графика
  • настройка на производителността в sql сървър стъпка по стъпка
  • разлика между и s corp и c corp
  • колко голяма е индустрията за красота
  • функции са тези, които вече са написани и са предоставени като част от системата.
  • калкулатор на заплата от служител до изпълнител
Категории
  • Пъргав
  • Иновация
  • Тенденции
  • Back-End
  • © 2022 | Всички Права Запазени

    portaldacalheta.pt