Алгоритм сопоставления линейных шаблонов?

У меня есть линейный список нулей и единиц, и мне нужно сопоставить несколько простых шаблонов и найти первое вхождение. Например, мне может понадобиться найти 0001101101, 01010100100, ИЛИ 10100100010 в списке длиной 8 миллионов. Мне нужно только найти первое вхождение любого из них, а затем вернуть индекс, по которому оно встречается. Однако выполнение циклов и доступ к большому списку может быть дорогостоящим, и я бы не хотел делать это слишком много раз.

Есть ли более быстрый метод, чем выполнение

foreach (patterns) {
    for (i=0; i < listLength; i++)
        for(t=0; t < patternlength; t++)
            if( list[i+t] != pattern[t] ) {
                 break;
            }
            if( t == patternlength - 1 ) {
                 return i;  // pattern found!
            }
        }
    }
}

Редактировать: Кстати, я реализовал эту программу в соответствии с приведенным выше псевдокодом, и производительность в порядке, но ничего особенного. По моим оценкам, я обрабатываю около 6 миллионов битов в секунду на одном ядре моего процессора. Я использую это для обработки изображений, и мне нужно будет обработать несколько тысяч 8-мегапиксельных изображений, так что каждая мелочь помогает.

Редактировать: Если неясно, я работаю с битовым массивом, поэтому есть только две возможности: ОДИН и НОЛЬ. И это на С++.

Редактировать: спасибо за указания на алгоритмы BM и KMP. Я заметил, что на странице Википедии для БМ написано

Алгоритм предварительно обрабатывает искомую целевую строку (ключ), но не искомую строку (в отличие от некоторых алгоритмов, которые предварительно обрабатывают искомую строку, а затем могут амортизировать затраты на предварительную обработку путем повторного поиска).

Это выглядит интересно, но не дало примеров таких алгоритмов. Что-то подобное тоже поможет?


person erjiang    schedule 09.08.2009    source источник
comment
Какой язык вы используете или можете использовать? Более совершенный алгоритм почти наверняка уже реализован для вас. (Бонус: какое оборудование вы используете? Арендуйте облако и сопоставьте/уменьшите проблему!)   -  person pilcrow    schedule 09.08.2009
comment
С++. Прямо сейчас моя программа состоит из пары сотен строк кода + один класс, который я dl'd... Я хочу, чтобы она была простой и больше не интегрировала сторонний код.   -  person erjiang    schedule 09.08.2009


Ответы (7)


Ключом для поиска в Google является сопоставление строк с несколькими шаблонами.

В 1975 году Ахо и Корасик опубликовали алгоритм (с линейным временем), который используется в оригинальной версии fgrep. Алгоритм впоследствии дорабатывался многими исследователями. Например, Commentz-Walter (1979) объединил Aho&Corasick с Boyer&Moore соответствие. Baeza-Yates (1989) объединил AC с Boyer-Moore-Horspool вариант. Ву и Манбер (1994) проделали аналогичную работу.

Альтернативой линейке алгоритмов сопоставления нескольких шаблонов AC является алгоритм Рабина и Карпа.

Я предлагаю начать с чтения страниц Википедии Ахо-Корасика и Рабина-Карпа, а затем решить, будет ли это иметь смысл в вашем случае. Если это так, возможно, уже есть реализация для вашего языка/среды выполнения.

person earl    schedule 09.08.2009
comment
Я лично рекомендую rabin-karp, так как он относительно хорош для поиска по нескольким шаблонам, а также очень прост в реализации. Единственная сложная часть — это скользящий хэш, о котором вы можете прочитать здесь (en.wikipedia.org/ вики/Rolling_hash) - person Falaina; 09.08.2009

да.

Алгоритм поиска строки Бойера-Мура

См. также: алгоритм Кнута-Морриса-Пратта

person Mitch Wheat    schedule 09.08.2009
comment
Да, и BM, и KMP будут работать быстрее, чем показанный псевдокод, но по-прежнему будут искать только один шаблон за раз. Таким образом, самый внешний foreach (patterns) {} все еще будет рядом. - person earl; 09.08.2009

Вы можете создать SuffixArray и выполнять поиск во время выполнения с ума: O ( length(pattern) ). НО .. вам нужно построить этот массив. Это стоит только .. когда текст статичен, а шаблон динамичен.

person n00ki3    schedule 09.08.2009

Решение, которое может быть эффективным:

  1. хранить шаблоны в структуре данных trie
  2. начать поиск в списке
  3. проверьте, есть ли следующие символы pattern_length в дереве, остановитесь в случае успеха (операция O (1) )
  4. шаг один символ и повторить # 3

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

person Nick Dandoulakis    schedule 09.08.2009

Если ваши строки должны быть гибкими, я бы также рекомендовал модифицированный «алгоритм поиска строк Бойера-Мура» в соответствии с Митчем Уитом. Если ваши строки не должны быть гибкими, вы сможете еще больше свернуть сопоставление с образцом. Модель Бойера-Мура невероятно эффективна для поиска в большом количестве данных одной из нескольких строк для сопоставления.

Джейкоб

person TheJacobTaylor    schedule 09.08.2009
comment
Что вы подразумеваете под гибким? - person erjiang; 09.08.2009
comment
Извините за это, я устал. По сути, я думал об алгоритме Турбо-Бойера-Мура (www-igm .univ-mlv.fr/~lecroq/string/node15.html), но не смог вспомнить его название. Преимущество BM в том, что он предварительно обрабатывает все искомые строки и просматривает основную строку один раз, чтобы найти любое совпадение для любой из строк. Вот почему он не выполняет предварительную обработку основной строки. Он делает только один проход. Конечно, базовый BM может иметь 3n сравнений (где n — размер искомой строки). Turbo BM ограничен 2n. - person TheJacobTaylor; 11.08.2009

Если это битовый массив, я полагаю, что выполнение скользящей суммы было бы улучшением. Если длина шаблона равна n, просуммируйте первые n бита и посмотрите, совпадает ли он с суммой шаблона. Всегда сохраняйте первый бит суммы. Затем для каждого следующего бита вычтите первый бит из суммы и добавьте следующий бит, и посмотрите, соответствует ли сумма сумме шаблона. Это позволит сохранить линейный цикл по шаблону.

Кажется, что алгоритм BM не так хорош для этого, как кажется, потому что здесь у меня есть только два возможных значения, ноль и единица, поэтому первая таблица не очень помогает. Вторая таблица может помочь, но это означает, что BMH в основном бесполезен.

Редактировать: в состоянии недосыпания я не мог понять BM, поэтому я просто реализовал эту скользящую сумму (это было очень просто), и это ускорило мой поиск в 3 раза. Спасибо тому, кто упомянул «катящиеся хэши». Теперь я могу искать два 30-битных шаблона в 321 750 000 битах за 5,45 секунды (и это в однопоточном режиме) по сравнению с 17,3 секунды раньше.

person erjiang    schedule 09.08.2009

Если это просто чередование 0 и 1, закодируйте свой текст как запуски. Серия из n нулей равна -n, а серия из n единиц равна n. Затем закодируйте строки поиска. Затем создайте функцию поиска, использующую закодированные строки.

Код выглядит следующим образом:

try:
    import psyco
    psyco.full()
except ImportError:
    pass

def encode(s):
    def calc_count(count, c):
        return count * (-1 if c == '0' else 1)
    result = []
    c = s[0]
    count = 1
    for i in range(1, len(s)):
        d = s[i]
        if d == c:
            count += 1
        else:
            result.append(calc_count(count, c))
            count = 1
            c = d
    result.append(calc_count(count, c))
    return result

def search(encoded_source, targets):
    def match(encoded_source, t, max_search_len, len_source):
        x = len(t)-1
        # Get the indexes of the longest segments and search them first
        most_restrictive = [bb[0] for bb in sorted(((i, abs(t[i])) for i in range(1,x)), key=lambda x: x[1], reverse=True)]
        # Align the signs of the source and target
        index = (0 if encoded_source[0] * t[0] > 0 else 1)
        unencoded_pos = sum(abs(c) for c in encoded_source[:index])
        start_t, end_t = abs(t[0]), abs(t[x])
        for i in range(index, len(encoded_source)-x, 2):
            if all(t[j] == encoded_source[j+i] for j in most_restrictive):
                encoded_start, encoded_end = abs(encoded_source[i]), abs(encoded_source[i+x])
                if start_t <= encoded_start and end_t <= encoded_end:
                    return unencoded_pos + (abs(encoded_source[i]) - start_t)
            unencoded_pos += abs(encoded_source[i]) + abs(encoded_source[i+1])
            if unencoded_pos > max_search_len:
                return len_source
        return len_source
    len_source = sum(abs(c) for c in encoded_source)
    i, found, target_index = len_source, None, -1
    for j, t in enumerate(targets):
        x = match(encoded_source, t, i, len_source)
        print "Match at: ", x
        if x < i:
            i, found, target_index = x, t, j
    return (i, found, target_index)


if __name__ == "__main__":
    import datetime
    def make_source_text(len):
        from random import randint
        item_len = 8
        item_count = 2**item_len
        table = ["".join("1" if (j & (1 << i)) else "0" for i in reversed(range(item_len))) for j in range(item_count)]
        return "".join(table[randint(0,item_count-1)] for _ in range(len//item_len))
    targets = ['0001101101'*2, '01010100100'*2, '10100100010'*2]
    encoded_targets = [encode(t) for t in targets]
    data_len = 10*1000*1000
    s = datetime.datetime.now()
    source_text = make_source_text(data_len)
    e = datetime.datetime.now()
    print "Make source text(length %d): " % data_len,  (e - s)
    s = datetime.datetime.now()
    encoded_source = encode(source_text)
    e = datetime.datetime.now()
    print "Encode source text: ", (e - s)

    s = datetime.datetime.now()
    (i, found, target_index) = search(encoded_source, encoded_targets)
    print (i, found, target_index)
    print "Target was: ", targets[target_index]
    print "Source matched here: ", source_text[i:i+len(targets[target_index])]
    e = datetime.datetime.now()
    print "Search time: ", (e - s)

Для строки вдвое большей, чем вы предложили, требуется около семи секунд, чтобы найти самое раннее совпадение из трех целей в 10 миллионах символов. Конечно, поскольку я использую случайный текст, он немного меняется при каждом запуске.

psyco — это модуль Python для оптимизации кода во время выполнения. Используя его, вы получаете отличную производительность, и вы можете оценить это как верхнюю границу производительности C/C++. Вот недавнее выступление:

Make source text(length 10000000):  0:00:02.277000
Encode source text:  0:00:00.329000
Match at:  2517905
Match at:  494990
Match at:  450986
(450986, [1, -1, 1, -2, 1, -3, 1, -1, 1, -1, 1, -2, 1, -3, 1, -1], 2)
Target was:  1010010001010100100010
Source matched here:  1010010001010100100010
Search time:  0:00:04.325000

Для кодирования 10 миллионов символов требуется около 300 миллисекунд, а для поиска по ним трех закодированных строк — около 4 секунд. Я не думаю, что время кодирования будет большим в C/C++.

person hughdbrown    schedule 09.08.2009
comment
Я обеспокоен тем, что время, необходимое для кодирования 8 миллионов битов в прогонах, выполнения нескольких поисков, а затем их декодирования обратно в необработанные биты, может занять больше времени, чем поиск BM или KMP в необработанных битах. - person erjiang; 09.08.2009
comment
Это, безусловно, возможность. - person hughdbrown; 09.08.2009