Самый быстрый способ найти минимальное расстояние Хэмминга до любой подстроки? - PullRequest
7 голосов
/ 18 июля 2009

Учитывая длинную строку L и более короткую строку S (ограничение состоит в том, что L .length должно быть> = S .length), я хочу найти минимальное расстояние Хэмминга между S и любая подстрока L с длиной, равной S .length. Давайте назовем функцию для этого minHamming(). Например,

minHamming(ABCDEFGHIJ, CDEFGG) == 1.

minHamming(ABCDEFGHIJ, BCDGHI) == 3.

Для этого очевидным образом (перечисление каждой подстроки L) требуется время O (S .length * L .length). Есть ли какой-нибудь умный способ сделать это в сублинейное время? Я ищу один и тот же L с несколькими различными строками S, поэтому выполнение сложной предварительной обработки до L, как только это допустимо.

Редактировать: Модифицированный Бойер-Мур был бы хорошей идеей, за исключением того, что мой алфавит состоит всего из 4 букв (ДНК).

Ответы [ 3 ]

15 голосов
/ 20 июля 2009

Возможно, удивительно, эта точная проблема может быть решена всего за O (| A | nlog n) время с использованием быстрых преобразований Фурье (FFT), где n - длина большей последовательности L и |A| - размер алфавита.

Вот свободно доступный PDF документ Дональда Бенсона, описывающий, как это работает:

Краткое описание: Преобразование каждой из ваших строк S и L в несколько векторов-индикаторов (по одному на символ, поэтому 4 в случае ДНК), а затем сворачивать соответствующих векторов для определения количества совпадений для каждого возможного выравнивания. Хитрость в том, что свертка во «временной» области, которая обычно требует времени O (n ^ 2), может быть реализована с использованием умножения в «частотной» области, которая требует только O (n) времени плюс время, необходимое для преобразования между доменами и обратно. При использовании БПФ каждое преобразование занимает всего O (nlog n) времени, поэтому общая сложность времени составляет O (| A | nlog n). Для наибольшей скорости используются конечное поле БПФ, для которых требуется только целочисленная арифметика.

Примечание: Для произвольных S и L этот алгоритм явно выигрывает в производительности по сравнению с простым алгоритмом O (mn), так как |S| и |L| становятся большими, но OTOH, если S обычно короче log|L| (например, при запросе большой БД с небольшой последовательностью), тогда очевидно, что этот подход не обеспечивает ускорения.

ОБНОВЛЕНИЕ 21/7/2009 : Обновлено, чтобы упомянуть, что сложность времени также линейно зависит от размера алфавита, поскольку для каждого символа в алфавите должна использоваться отдельная пара векторов-индикаторов.

2 голосов
/ 18 июля 2009

Модифицированный Бойер-Мур

Я только что откопал какую-то старую реализацию Python Boyer-Moore Я лежал и изменил цикл сопоставления (где текст сравнивается с шаблоном). Вместо разрыва, как только первое несоответствие найдено между двумя строками, просто подсчитайте количество несоответствий, но запомните первое несоответствие :

current_dist = 0
while pattern_pos >= 0:
    if pattern[pattern_pos] != text[text_pos]:
        if first_mismatch == -1:
            first_mismatch = pattern_pos
            tp = text_pos
        current_dist += 1
        if current_dist == smallest_dist:
           break

     pattern_pos -= 1
     text_pos -= 1

 smallest_dist = min(current_dist, smallest_dist)
 # if the distance is 0, we've had a match and can quit
 if current_dist == 0:
     return 0
 else: # shift
     pattern_pos = first_mismatch
     text_pos = tp
     ...

Если строка не полностью совпадает в этой точке, вернитесь к точке первого несоответствия, восстановив значения. Это гарантирует, что наименьшее расстояние действительно найдено.

Вся реализация довольно длинная (~ 150LOC), но я могу опубликовать ее по запросу. Основная идея изложена выше, все остальное - стандартный Бойер-Мур.

Предварительная обработка текста

Еще один способ ускорить процесс - это предварительная обработка текста для получения индекса по позициям символов. Вы хотите начать сравнение только с позиций, где происходит хотя бы одно совпадение между двумя строками, в противном случае расстояние Хэмминга равно | S | тривиально.

import sys
from collections import defaultdict
import bisect

def char_positions(t):
    pos = defaultdict(list)
    for idx, c in enumerate(t):
        pos[c].append(idx)
    return dict(pos)

Этот метод просто создает словарь, который отображает каждый символ в тексте в отсортированный список его вхождений.

Цикл сравнения более или менее неизменен по сравнению с наивным O(mn) подходом, за исключением того факта, что мы не увеличиваем позицию, в которой сравнение начинается каждый раз на 1, но на основе позиций символов:

def min_hamming(text, pattern):
    best = len(pattern)
    pos = char_positions(text)

    i = find_next_pos(pattern, pos, 0)

    while i < len(text) - len(pattern):
        dist = 0
        for c in range(len(pattern)):
            if text[i+c] != pattern[c]:
                dist += 1
                if dist == best:
                    break
            c += 1
        else:
            if dist == 0:
                return 0
        best = min(dist, best)
        i = find_next_pos(pattern, pos, i + 1)

    return best

Фактическое улучшение в find_next_pos:

def find_next_pos(pattern, pos, i):
    smallest = sys.maxint
    for idx, c in enumerate(pattern):
        if c in pos:
            x = bisect.bisect_left(pos[c], i + idx)
            if x < len(pos[c]):
                smallest = min(smallest, pos[c][x] - idx)
    return smallest

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

find_next_pos, безусловно, сложно, и можно попытаться улучшить его, используя только первые несколько символов шаблона S, или использовать набор, чтобы убедиться, что символы из шаблона не проверяются дважды.

Обсуждение

Какой метод быстрее, во многом зависит от вашего набора данных. Чем разнообразнее ваш алфавит, тем больше будет скачков. Если у вас очень длинный L, второй метод с предварительной обработкой может быть быстрее. Для очень, очень коротких строк (как в вашем вопросе) наивный подход, безусловно, будет самым быстрым.

ДНК

Если у вас очень маленький алфавит, вы можете попытаться получить положение символов для биграмм (или больше), а не для униграмм.

0 голосов
/ 18 июля 2009

Вы застряли в том, что касается big-O .. На фундаментальном уровне вам нужно будет проверить, соответствует ли каждая буква в цели каждой подходящей букве в подстроке.

К счастью, это легко распараллелить.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...