catpad: (Default)
[personal profile] catpad

Задача пришла из жизни. Это не просто упражнение, а совершенно реальная задача, которую мне необходимо решить на работе. Я её решил, но, может быть, кто-то придумает лучше.


Дано множество строк, например:

voda
eric
nec

и так далее - размер множества любой.
На входе алгоритм получает произвольную строку, например: "vodafone.com".
Как наиболее быстрым способом определить, что эта строка содержит в качестве подстроки (substring) одну из данного множества ?
(В приведённом примере ответ положительный, потому что "voda" - это подстрока "vodafone.com").

Задача простая, но ключевые слова здесь, конечно - "наиболее быстро".
Я придумал как это сделать почти за линейное время. "Почти" в смысле, что я не очень хорошо помню, что такое "amortized complexity", но, кажется, получается как раз O(n).
Завтра опубликую ответ, но хотелось бы посмотреть на разные предложения.

Date: 2005-12-01 02:05 am (UTC)
From: [identity profile] pigglet.livejournal.com
м-м... может я сейчас не сильно сображаю, но в чём задача-то? Практически все языки пргрммрвн имеют у себя в запасе набор функций (или как там оно называется у нефункцинальных языков)для решения этой задачи. Разве использование этой самой родной для языка функции не будет самым оптимальным ходом? Или вопрос в том, как её применять? Разве есть варианты, кроме перебора первого множества? (Разве что сложить множество в одну большую строку и сверять просто две строки на совпадение хоть кусочка :))))

Date: 2005-12-01 04:18 am (UTC)
From: [identity profile] elephantum.livejournal.com
надо собрать такой здоровый regexp (он же недетерминированый КА)
.*((voda)|(eric)|(nec)).*

откомпилировать в детерминированый КА и за линейное время определить принадлежит ли одно из ключевых слов входному и заодно определить какое.

расходы на компиляцию константные. матчинг по детерминированному КА - линейное время.

подробнее читать - Ахо и Ульмана, либо найти либу по работе с regexp'ами, которая так уже делает.

Date: 2005-12-01 10:24 am (UTC)
From: [identity profile] potan.livejournal.com
Констнантные для произвольного регуляра? А если он сравним по объему со всем текстом.
Кроме того, для одной строки известны алгоритмы, которые ищут ее быстрее, чем если ее интерпретировать как регулярное выражение (они заглядывают вперед, и смотрят на сколько символов надо сместиться - получается в константу, зависящую от длины искомой строки, раз быстрее). Эти алгоритмы должны обобщаться и на регулярные выражения, но мне такие не известны.

Date: 2005-12-01 10:30 am (UTC)
From: [identity profile] elephantum.livejournal.com
я не говорил про произвольный регуляр. вид регуляра я задал в своем посте. вообще регуляр я вспомнил только потому что NDFSM рисовать лень было.
From: [identity profile] sashanep.livejournal.com
Может быть я не понял твою задачу правильно,но:
Если у тебя K заданных строчек и входящая строка длиной N, то тебе нужно сравнить N^2/2 подстрочек заданной строки с K заданными строчками.
Если заданные строчки организованы в таблицу по алфавиту (константная сложность), то сравнение N^2/2 подстрочек заданной строки - сложность О(N^2), а не линейная...
From: [identity profile] sashanep.livejournal.com
или N у тебя - это количество заданных строк, а К - длина входящей строки?
Тогда да, сложность задачи линейна по N - проверка каждой подстроки (всего их К^2/2) на идентичность с одной из N заданных строк - О(N*К^2/2).

(скромное мнение)

Date: 2005-12-01 06:50 am (UTC)
From: [identity profile] satangel.livejournal.com
видимо в 2 этапа:
отсев заведомо непригодных вариантов - взаимное сравнение "сэт перых букв подстрок" с набором букв строки - и подстроки просеятся если там хоть что-то есть
а потом тупой поиск подстроки в строке, в идеальном случае разбить на вменяемые подмножества и распараллелить...

Date: 2005-12-01 06:59 am (UTC)
From: [identity profile] motya.livejournal.com
Примерно так:

Препроцессинг: хэщируем строки, таким образом, доступ к нужной строке будет О(1).
Определяем множество (это может быть связный список, это не важно) кандидатов, в котором будут сидеть строки (или их инжекс в хэше) и для каждой строки еще поле "проверено до Х буквы". Сейчас это множество пусто.

Для каждой буквы из инпута проделываем следущее: для каждого кандидата из множества сравниваем букву из инпута с Х+1 (где Х - "проверено до ... буквы") буквой кандидата. Если эти буквы разные, выкидываем кандидата из списка, если одинаковые инкрементируем его "проверено до ... буквы". Далее, берем из хэша все слова начинающиеся на эту букву (ту, что считали из инпута) и добавляем в список кандидатов, устанавливая "проверено до ... буквы" в 1.

Алгоритм останавливается, если "проверено до ... буквы" для некоторого кандидата будет равно количеству букв в нем. В этом случае ответом будет этот самый кандидат. Если такового не нашлось, а инпут кончился - значит не судьба...

В нормальном случае сложность такого алгоритма О(длина инпута).

Date: 2005-12-01 07:04 am (UTC)
From: [identity profile] motya.livejournal.com
Дополнение: ключ хэша - буквы (первая, затем, если слишком много на какой-нибудь букве сидит, то там сабхэш по второй букве и т.д.).
Понятно, что одна и та же строка может быть в списке кандидатов несколько раз с разным "проверено до ... буквы".

Date: 2005-12-02 02:12 am (UTC)
From: [identity profile] voldemar.livejournal.com

Следующий вариант вырисовывается.

Предварительно по множеству строк строим "дерево чтения строк". Построение начинается с помечнного пустым символом корня дерева. К нему добавляются дочерние вершины, помеченные первыми буквами строк данного множества. Затем к каждой из этих вершин - дочерние вершины, помеченные вторыми буквами слов, начинающихся с соответствующей буквы. И так далее. Таким образом, продвигаясь вглубь по ветвям дерева мы будем читать слова данного множества.

Image

Причем при построении "дерева" часть строк из исходного множества может отпасть. А именно те из них, которые начинаются со вхождения более короткой строки из данного множества. Например, если во множестве строк имеются строки "eric" и "ericson", то вторую строку можно очевидно не принимать в расчет (поскольку уж если большая подстрока встречается во "входной строке", то уж меньшая и подавно) и не включать в "дерево чтения".

Далее нам понадобится множество для хранения указателей на текущие анализируемые вершины дерева. Изначально оно пусто.

Теперь для каждого символа из "входной строки" выполняем:

  1. для каждого указателя из множества проверить, есть ли среди дочерних вершин одна, помеченная текущим символом "входной строки"
    1. если такая дочерняя вершина есть - переставляем указатель на нее. Если вершина к тому же является листом - алгоритм закончен, мы нашли подстроку
    2. если такой дочерней вершины нет - удаляем указатель из множества
  2. если среди дочерних вершин корня дерева имеется одна, помеченная текущим символом "входной строки" поместим указатель на нее во множество указателей

(фактически указатель на вершину дерева будет нам говорить о том, что мы смогли начиная с некоторой позиции "входной строки" прочитать последовательность букв, ведущую от корня "дерева строк" к указанной вершине дерева)

Если мы проанализировали все символы "входной строки", значит ни одна из заданного множества строк не является подстрокой "входной строки".

Чтобы оценить трудозатраты алгоритма, заметим, что для каждого символа "входной строки" мы должны проверить каждый указатель вершины из множества. Значит нам нужно оценить верхнюю границу мощности множества указателей на текущие вершины "дерева чтения строк". Поскольку на каждом шаге :

  1. мы можем добавить ко множеству указателей один единственный указатель (на текущую букву "входной строки" в корне "дерева чтения строк")
  2. каждый ранее введенный указатель продвигается на один уровень вглубь "дерева чтения" или исчезает (в случае если нет соответствующих букв для продвижения)
то максимально возможная мощность множества указателей равна глубине дерева. Итого, в худшем случае нам придется выполнить N*M операций проверки-перемещения указателей. Где N - длина "входной строки", а M - глубина "дерева чтения", или максимальная длина строки из заданного множества.

Date: 2005-12-02 02:31 am (UTC)
From: [identity profile] catpad.livejournal.com
Всё верно!
Ну вот, вы тоже изобрели trie, поздравляю! :)

Date: 2005-12-02 03:02 pm (UTC)
From: [identity profile] voldemar.livejournal.com
Спасибо за термин, запомню на будущее

Date: 2005-12-02 08:59 am (UTC)
From: [identity profile] green-fr.livejournal.com
Вова, это офигительно напоминает алгоритм расшифровки морзянки :-)
И - как следствие - архивирование (за названием к Кнуту), где дерево строится так, что наиболее частые символы висят ближе к корню, т.е. кодируются меньшим количеством бит. Только у твоего дерева алфавит больше.

Date: 2005-12-02 03:00 pm (UTC)
From: [identity profile] voldemar.livejournal.com
В архивировании trie (спасибо catpad'у за термин - запомню) может использоваться для все того же - поиска подстрочек, например внутри алгоритма LZW (http://algolist.manual.ru/compress/standard/lzw.php). Но в tire не наблюдается никакой связи удаленности узлов от корня с частотой появления (частотой появления где?) помечающего этот узел символа не наблюдается. Уже хотя бы потому что одним и тем же символом могут быть помечены несколько вершин: в trie важна только последовательность чтения символов.
Но главное, ты, по-моему запутался в двух алфавитах :-) У Хаффмана (http://algolist.manual.ru/compress/standard/huffman.php), про которого ты судя по всему и вспомнил, есть два алфавита: кодируемый {a..z} (то что ты называешь символами, "висящими ближе к корню") и кодирующий {0,1} (то что ты называешь "алфавитом"). Да, в результате алгоритма построения бинарного словаря по Хаффману получается trie для кодирующего алфавита {0, 1}. Фактически, мы получаем частный случай trie - бинарное дерево. А символами кодируемого алфавита {a-z} помечаются только листья. Тогда, читая закодированный (сжатый поток) посимвольно (побитно) и перемещаясь по trie (бинарному дереву), мы сможем прочесть закодированное сообщение в алфавите {a..z}, если будем запоминать последовательность посещенных листьев. А тот факт, что мы строим trie таким специальным образом, что чаше встречающиеся символы кодируемого алфавита подвешены на конце более коротких "веточек", позволяет нам закодировать исходное сообщение в алфавите {a..z} более компактным образом в алфавите {0,1}.

Date: 2005-12-02 03:22 pm (UTC)
From: [identity profile] green-fr.livejournal.com
Точно, Хаффман.
Я разницу вижу, я просто и сходство заметил :-)
(deleted comment)
(deleted comment)

Date: 2005-12-02 05:53 am (UTC)
From: [identity profile] catpad.livejournal.com
Да я на самом деле уже там дальше всё написал.
Спасибо за ссылки.

Date: 2005-12-02 09:00 am (UTC)
From: [identity profile] green-fr.livejournal.com
Форму поиска на сайте пишешь? :-)

Date: 2005-12-02 09:04 am (UTC)
From: [identity profile] catpad.livejournal.com
Нет,
это называется Access Point Name. Когда ты с телефона пользуешься каким-то интернет-сервисом, эта штука записывается в call description record, а мне её надо искать в таблице и извлекать всякие данные, которые с этим адресом связаны.
Page generated Feb. 6th, 2026 08:03 pm
Powered by Dreamwidth Studios