Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Алгоритмы обработки данных. Учебное пособие

.pdf
Скачиваний:
0
Добавлен:
12.08.2026
Размер:
1 Мб
Скачать

 

 

 

k = 0: a, a, b, a

 

 

 

 

pos = {3,1,0,2}

 

 

 

 

 

 

k = 1: aa, ab, ba, aa

 

 

 

pn = { 2, 0, 3, 1 }pos = {0,3,1,2}

 

 

 

 

 

 

k = 2: aaba, abaa, baaa, aaab pn = { 2 ,1 ,3 ,0 }

 

 

 

 

 

 

 

Рис. 2.41. Соотношение массивов pos и pn

 

 

 

 

 

1

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Вывод pos

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

3

 

 

 

k = 0, 1,.., log(n)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Конец

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

i = 0,1, .., n – 1

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

cnt[i] = 0; i = 0,1,..,clas

 

 

 

 

 

 

 

 

pn[i] = pos[i] – 2k

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Нет

 

 

 

 

i = 0,1,..,n – 1

 

 

 

 

 

 

 

pn[i] < 0

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

cnt[C[pn[i]]] = cnt[C[pn[i]]] + 1

 

 

 

 

 

 

 

 

Да

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

pn[i] = pn[i] + n

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

i = 1,2,..,clas

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

cnt[i] = cnt[i]+cnt[i – 1]

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

i = n – 1, n – 2,..,0

 

 

2

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

pos[cnt[C[pn[i]]]]=pn[i];

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

cnt[C[pn[i]]] = cnt[C[pn[i]]]

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Рис. 2.42. Основной цикл: фрагмент, соответствующий сортировке

2

cn[pos[0]] = 0;

 

 

 

clas = 0;

 

 

 

 

i = 1,2,.., n – 1

C[i] = cn[i] для i = 0,1,..,clas

3

 

mid1

= (pos[i] + 2k)mod(n)

 

 

 

mid2

= (pos[i 1] + 2k)mod(n)

 

 

 

Нет

C[pos[i]] != C[pos[i – 1]]

 

 

Да

или

 

 

 

C[mid1] != C[mid2]

 

 

 

clas = clas + 1

 

 

 

 

cn[pos[i]] = clas

 

 

Рис. 2.43. Основной цикл: вычисление классов эквивалентности

51

Для вычисления классов эквивалентности в текущей фазе необходимо заглянуть в массив C предыдущей фазы и сравнить две пары значений, соответствующих половинам текущих подстрок. Фрагмент алгоритма соответствующей части тела основного цикла, выполняющий перестроение массива C, показан на рис. 2.43.

2.4.2. ПОИСК ПОДСТРОКИ

Для поиска образца p длиной m в тексте t длиной n с использованием суффиксного массива pos необходимо при помощи двоичного поиска найти наименьший индекс i такой, что t[pos[i − 1]..n – 1] не начинается с p, и наибольший i′, что t[pos[i′ + 1]..n – 1] не начинается с p, пример и алгоритм двоичного поиска в массиве чисел показаны

на рис. 2.44, 2.45 ( – округление к меньшему целому). При использо-

вании двоичного поиска для строк время выполнения каждого сравнения зависит от числа сравниваемых символов – m сравнений в худшем случае. Пессимистичная оценка времени работы O(mlog(n)) достигается при наличии в t большого количества длинных префиксов p.

L = 0, R = Длина(A);

P = 3

 

 

Пока L > R Цикл

A = { 1 2 3 3 3 4 5 }

 

 

 

 

 

M = (L+R)/2 ;

 

L

M

R

Если A[M] < P Тогда

 

 

 

 

 

L = M + 1;

 

{ 1 2 3 3 3 4 5 }

Иначе

 

L M

R

 

R = M;

 

 

 

 

 

 

Конец если

 

{ 1 2 3 3 3 4 5 }

Конец цикла

 

L M R

 

 

 

 

 

 

 

 

 

Рис. 2.44. Поиск первого вхождения числа P в массиве A

Поиск можно ускорить, если запоминать количество совпадающих символов. Пусть L и R – левая и правая границы поиска соответственно. Если длину префикса t[pos[L]..n – 1], совпадающего с префиксом p, обозначить l, а соответствующую длину для правой границы обозна-

чить r, то при очередном сравнении в позиции M = (R + L)/2 можно начинать обрабатывать символы не с первой позиции, а с min(l, r) + 1. Такое улучшение позволяет достигать оценки O(m + log(n)) на «хороших» входных данных, однако в худшем случае остается O(mlog(n)).

52

L = 0, R = Длина(A);

P = 3

 

 

 

 

 

 

A = { 1 2 3 3 3 4 5 }

Пока L > R Цикл

 

 

 

 

 

 

 

 

M = (L+R)/2 ;

 

L

 

 

M

 

R

Если A[M] < P Тогда

 

 

 

 

 

 

 

 

 

 

 

 

{ 1

2

3

3

3

4

5 }

L = M + 1;

 

L M

 

R

 

 

Иначе

 

 

 

 

R = M;

 

{ 1 2 3 3 3 4 5 }

Конец если

 

 

L M R

 

 

Конец цикла

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Рис. 2.45. Поиск последнего вхождения числа P в массиве A

Для гарантированного уменьшения количества дополнительных сравнений на шаге бинарного поиска можно использовать дополнительную информацию – длину наибольшего общего префикса (longest common prefix) LCP. Пусть LCP(i, j) – длина наибольшего общего префикса суффиксов pos[i] и pos[j]. Тогда, если l не равно r, а, например, больше, то соотношение LCP(L, M) и l поможет сделать дальнейшие выводы. Если LCP(L, M) меньше l, то p совпадает с суффиксом pos(L) на большее количество символов, чем с суффиксом pos(M), следовательно, нужно продолжить поиск в левой части и изменить значение r

на LCP(L, M). Если LCP(L, M) больше l, то суффиксы pos(L) и pos(M)

совпадают до (l + 1)-го символа, при этом суффикс pos(L) в позиции (l + 1) уже отличается от p, следовательно, нужно продолжить поиск в правой части, а l оставить без изменений. Только в случае равенства LCP(L, M) и l необходимо сравнивать соответствующие символы суффикса pos(M) и строки p, начиная с (l + 1)-го, до тех пор, пока не встретятся различные символы. Ситуация, в которой r больше l, обрабатывается аналогично. Таким образом, время работы поиска будет складываться из log(n) шагов, на которых в сумме производится m сравнений символов, но на каждом шаге необходимо вычисление LCP(i, j).

Один из способов определения LCP(i, j), требующий log(n) времени и nlog(n) памяти, состоит в следующем. Массивы классов эквивалентности C, которые использовались при построении, содержат информацию о равенстве подстрок строки t. На k-й фазе алгоритма это подстроки длиной 2k. Каждый суффикс можно представить как конкатенацию таких подстрок (начиная с наибольшей), как показано на рис. 2.46.

53

t = t0 t1 t2 t3 t4 t5 t6 t7 t8 t9 t10 t11 t12 t13 t14 t15 t16 t17 t18 t19 t20 t21

t = t0 t1 t2 t3 t4 t5 t6 t7 t8 t9 t10 t11 t12 t13 t14 t15 t16 t17 t18 t19 t20 t21

t = t0 t1 t2 t3 t4 t5 t6 t7 t8 t9 t10 t11 t12 t13 t14 t15 t16 t17 t18 t19 t20 t21

Рис. 2.46. Суффиксы как конкатенация подстрок различных фаз алгоритма

Результат =0;

Для k = log(n) , log(n)/2, log(n)/4,..,0 Цикл Если Ck[k][i] = Ck[k][j] Тогда

Результат = Результат + 2k; i= i +2k;

j= j +2k;

Конец если Конец цикла

Рис. 2.47. Алгоритм вычисления LCP(i, j)

Пусть Ck[k] – массив классов эквивалентности k-й фазы. Чтобы определить LCP(i, j), нужно перебирать степени двойки (от большей к меньшей) и проверять, совпадают ли подстроки соответствующей длины. Если совпадают, то к ответу следует прибавить эту степень двойки, а наибольший общий префикс продолжить искать справа от одинаковой части. Алгоритм вычисления LCP(i, j) представлен на рис. 2.47.

2.5. НАИБОЛЬШАЯ ОБЩАЯ ПОДПОСЛЕДОВАТЕЛЬНОСТЬ

Согласно определению, подпоследовательностью последовательности {xn} называется последовательность xnk , составленная из некото-

рых членов {xn}, взятых в порядке возрастания номеров nk. Иначе говоря, чтобы получить подпоследовательность, можно выбрать некоторые элементы исходной последовательности, не меняя их взаимного расположения, или (в случае, если исходная последовательность конечна) удалить из

54

нее некоторое количество элементов. Например, для строки ‘abcdef’ строки ‘acf’, ‘bd’, ‘bcef’ и будут подпоследовательностями.

Наибольшей общей подпоследовательностью (longest common subsequence, LCS) строк x и y будет строка наибольшей длины, одновременно являющаяся подпоследовательностью x и y. Длина LCS может представлять интерес как самостоятельная характеристика. Она тесно связана с редакционным расстоянием – минимальным количеством операций (вставки, удаления или замены символа), необходимых для преобразования одной строки в другую. Редакционное расстояние впервые упомянуто в работе советского математика Владимира Иосифовича Левенштейна [14]. Очевидно, что длина LCS является величиной, обратной расстоянию Левенштейна: чем больше совпадающая подпоследовательность, тем меньше операций необходимо для преобразования.

Нахождение LCS может быть полезным не только при сравнении текстовых строк, но и в задачах биоинформатики при анализе ДНК.

2.5.1. РЕКУРСИВНЫЙ ПОИСК ДЛИНЫ LCS

Простейший алгоритм поиска длины LCS представлен на рис. 2.48. Каждый рекурсивный вызов сравнивает только первые символы строк, на входе в рекурсию одна из строк или обе лишаются первого символа. Это происходит до тех пор, пока одна из строк (или обе) не станет пустой. Важно отметить, что в случае несовпадения символов порождается два вызова. Пример работы алгоритма показан на рис. 2.49, решение задачи представлено в виде дерева. Очевидным недостатком алгоритма является то, что одинаковые подзадачи (выделены на рис. 2.49 овалом) решаются несколько раз. На рис. 2.50 представлено дерево решений для другого примера, в котором у входных строк нет совпадающих символов (пустыми). Это двоичное дерево высотой (|x| + |y| – 1), что позволяет оценить временную сложность алгоритма как O(2(|x| + |y|)).

Значение, которое возвращает каждый рекурсивный вызов, можно интерпретировать как длину LCS некоторых суффиксов x и y. Количество различных сочетаний суффиксов исходных строк – это |x||y|. Если в процессе работы запоминать решения подзадач и обращаться к ним вместо повторных расчетов, то временная сложность значительно уменьшится и составит O(|x||y|), но это потребует такого же объема памяти. Псевдокод рекурсивного алгоритма с запоминанием представлен на рис. 2.51. Невыполнение условия в строке (6) означает, что для данной подзадачи решение уже найдено и вся «ветка» рекурсии, которая могла бы быть запущена, отсекается.

55

lcslen(x, y)

 

 

 

 

 

 

 

 

 

 

 

Да

 

 

 

 

 

 

 

|x |= 0 или |y| = 0

 

 

 

 

 

 

 

 

 

 

L = 0

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Нет

Да

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

x[0] = y[0]

 

 

 

 

 

 

 

 

 

L= 1 + lcslen(x[1..|x| – 1], y[1..|y| –1])

 

 

 

 

 

 

 

 

 

Нет

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

L = max( lcslen(x[1..|x| – 1], y), lcslen(x, y[1..|y| – 1]))

 

Вернуть L

Рис. 2.48. Алгоритм рекурсивного поиска длины LCS

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

a

 

 

 

 

“”

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

cda

 

 

 

 

 

 

da

 

 

 

 

a

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

“”

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

ca

 

 

 

 

 

 

a

 

 

 

 

da

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

abcda

 

 

 

 

 

bcda

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

“”

 

 

 

 

 

a

 

 

 

 

“”

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

da

 

 

 

 

 

a

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

aca

 

 

 

 

 

ca

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

“”

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

cda

 

 

 

 

a

 

 

 

 

 

da

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

bcda

 

 

a

 

 

 

 

 

 

 

 

 

 

 

 

“”

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

a

 

 

 

 

 

 

 

 

 

 

 

 

 

 

cda

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

bcda

 

 

 

 

“”

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

“”

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Рис. 2.49. Поиск LCS для строк x=abcda и y=aca

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

abc

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

def

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

bc

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

abc

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

def

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

ef

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

c

 

 

 

 

 

 

 

 

 

 

 

 

 

bc

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

bc

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

abc

 

def

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

ef

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

ef

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

ef

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

“”

 

 

 

c

 

 

 

 

 

 

c

 

 

 

 

 

 

 

bc

 

 

 

c

 

 

 

 

 

 

 

bc

 

 

 

 

 

bc

 

abc

def

 

 

ef

 

 

 

 

 

 

 

ef

 

 

 

 

 

 

 

f

 

 

 

 

 

ef

 

 

 

 

 

 

 

f

 

 

 

 

 

 

f

 

 

 

 

“”

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

“”

 

c

 

 

“”

 

 

c

c

 

bc

 

“”

c

c

 

 

bc

 

c

 

 

 

bc

 

 

 

ef

 

 

f

 

 

 

ef

 

 

f

 

 

f

 

 

“”

 

 

ef

 

f

 

 

f

 

 

“”

 

 

f

 

 

 

“”

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

“”

 

“”

 

 

“”

 

“”

 

“”

 

“”

 

 

 

 

“”

 

“”

 

“”

 

“”

 

“”

 

“”

 

 

 

 

 

 

 

 

 

 

 

 

 

 

“”

 

“”

 

 

 

“”

 

“”

 

 

“”

 

“”

 

 

 

 

 

“”

 

“”

 

 

“”

 

“”

 

 

“”

 

 

“”

 

 

 

 

 

 

 

 

Рис. 2.50. Поиск LCS для строк x = abc и y = def

56

(1)m = |x|; n = |y|;

(2)выделить память для двумерного массива L;

(3)L[i][j] = –1; для i = 0,..,m; j = 0,..,n;

(4)Вернуть lcs_len(0,0);

(5)Процедура lcs_len(i,j)

(6)Если L[i][j] < 0 Тогда

(7)Если i = m или j = n Тогда

(8)L[i][j] = 0;

(9)Иначе

(10)Если x[i] = y[j] Тогда

(11)L[i][j] = 1 + lcs_len(i + 1, j + 1);

(12)Иначе

(13)L[i][j] = max(lcs_len(i+1,j), lcs_len(i,j+1));

(14)Конец если

(15)Конец если

(15)Конец если

(16)Вернуть L[i][j];

(17)Конец процедуры

Рис. 2.51. Рекурсивный алгоритм с запоминанием

2.5.2. ДИНАМИЧЕСКОЕ ПРОГРАММИРОВАНИЕ

Итерационный алгоритм поиска LCS, в котором решения всех подзадач вычисляются последовательно от меньших к большим, является частным случаем широкого подхода к решению оптимизационных задач, известного как «динамическое программирование». На рис. 2.52 представлен алгоритм заполнения массива L, каждый элемент которого равен длине LCS различных суффиксов исходных строк. Элемент с индексами i, j соответствует LCS строк x[i..|x| – 1] и y[j..|y| – 1]. Пример заполненного массива показан на рис. 2.53. Окончательный результат – длина LCS строк x и y находится в ячейке с индексами 0, 0. Если требуется определить не только длину, но и саму подпоследовательность, то нужно проделать путь из этой ячейки вправо и вниз, выбирая направление с большим значением. Позиции, в которых символы совпадают, составляют искомую последовательность. Алгоритм вывода LCS и пример его работы показаны на рис. 2.54.

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

57

(1)m = |x|; n = |y|;

(2)выделить память для двумерного массива L;

(3)Для i = m, (m – 1),..,0 Цикл

(4)Для j = n, (n – 1),..,0 Цикл

(5)Если i = m или j = n Тогда

(6)L[i][j] = 0;

(7)Иначе

(8)Если x[i] = y[j] Тогда

(9)L[i][j] = 1 + L[i + 1][j + 1];

(10)Иначе

(11)L[i][j] = max(L[i+1][j], L[i][j+1]);

(12)Конец если

(13)Конец если

(14)Конец цикла

(15)Конец цикла

 

 

 

 

 

Рис. 2.52. Итерационный алгоритм поиска LCS

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

a

f

 

 

b

 

c

c

 

b

 

f

d

 

 

 

 

 

 

 

 

 

 

 

a

4

 

3

 

 

3

2

2

 

2

1

1

0

 

 

fd

 

 

 

 

 

 

 

e

3

 

3

 

 

3

2

2

 

2

1

1

0

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

e

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

d

 

 

 

b

3

 

3

 

 

3

2

2

 

2

1

1

0

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

2

 

2

 

 

2

2

2

 

1

1

1

0

 

 

 

 

 

 

e

 

 

 

c

 

 

 

 

 

 

 

 

 

 

 

d

1

 

1

 

 

1

1

1

 

1

1

1

0

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

fd

 

 

 

 

 

 

e

0

 

0

 

 

0

0

0

 

0

0

0

0

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

“”

 

 

 

 

 

 

0

 

0

 

 

0

0

0

 

0

0

0

0

 

 

 

 

 

 

Рис. 2.53. Заполненный массив L для строк х=‘aebcde’

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

и y=‘afbccbfd’

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

a

f

b

 

c

 

c

b

 

f

d

 

 

 

 

 

(1)

s = “”; i = 0; j = 0; m = |x|; n = |y|;

 

 

a

4

3

3

 

2

 

2

2

 

1

1

 

0

 

 

(2)

Пока i <m и j <n Цикл

 

 

e

3

3

3

 

2

 

2

2

 

1

1

 

0

 

 

(3)

 

Если x[i] = y[j] Тогда

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

(4)

 

 

Добавить символ x[i] к s;

 

 

b

3

3

3

 

2

 

2

2

 

1

1

 

0

 

 

 

 

 

 

 

 

 

 

 

(5)

 

 

j = j + 1; i = i + 1;

 

 

c

2

2

2

 

2

 

2

1

 

1

1

 

0

 

 

(6)

 

Иначе

 

 

d

1

1

1

 

1

 

1

1

 

1

1

 

0

 

 

(7)

 

 

Если L[i + 1][j] L[i][j + 1] Тогда

e

0

0

0

 

0

 

0

0

 

0

0

 

0

 

 

(8)

 

 

 

i = i + 1;

 

 

 

0

0

0

 

0

 

0

0

 

0

0

 

0

 

 

(9)

 

 

Иначе

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

(10)

 

 

 

j = j + 1;

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

(11)

 

 

Конец если

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

(12)

 

Конец если

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

(13)

Конец цикла

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Рис. 2.54. Вывод LCS для строк х=‘aebcde’ и y=‘afbccbfd’

58

Нидлмана–Вунша [15]. Принцип динамического программирования лежит в основе алгоритма выравнивания двух последовательностей Вагнера–Фишера [16].

2.5.3.АЛГОРИТМ ХИРШБЕРГА

Валгоритме, разработанном Хиршбергом [17], используется особый способ разбиения задачи на две меньшие так, чтобы сумма их решений совпадала с решением исходной. Примеры подходящего и неподходящего разбиения показаны на рис. 2.55. Первую строку можно разделить пополам, а для выбора варианта разбиения второй необходимо провести расчеты.

|LCS(”aca”, ”abcda”)| = 3;

|LCS(”ac”, ”ab”)| + |LCS(”a”, ”cda”)| = 1+1=2; |LCS(”ac”, ”abc”)| + |LCS(”a”, ” da”)| = 2+1=3;

Рис. 2.55. Примеры разбиения задачи поиска LCS

 

 

a

f

b

c

c

b

f

d

 

 

 

0

0

0

0

0

0

0

0

0

 

 

a

0

1

1

1

1

1

1

1

1

 

 

e

0

1

1

1

1

1

1

1

1

 

 

b

0

1

1

2

2

2

2

2

2

 

 

 

 

2

2

2

2

2

1

1

1

0

c

 

 

1

1

1

1

1

1

1

1

0

d

 

 

0

0

0

0

0

0

0

0

0

e

 

 

0

0

0

0

0

0

0

0

0

 

 

 

a

f

b

c

c

b

f

d

 

 

Рис. 2.56. Критерий выбора разбиения строки y=‘afbccbfd’

при х=‘aebcde’

Если для второй строки и правой половины первой строки заполнить матрицу решений подзадач в соответствии с предыдущим алго-

59

ритмом, то верхняя строка (заполненная последней) будет содержать длины LCS правой половины первой строки и суффиксов второй. Для левой половины матрица заполняется в обратном порядке и будет содержать длины LCS левой половины первой строки и префиксов второй. Для выбора подходящего разбиения нужно определить такие префикс и суффикс, для которых суммарная длина LCS со своими половинами первой строки будет максимальной. В примере, показанном на рис. 2.56, подходят две пары, для которых сумма длин LCS равна четырем.

Если продолжить рекурсивно разбивать задачи, то одна из строк станет пустой или сократится до одного символа. Если одна из строк пустая, то результат равен нулю. Если строка сократилась до одного символа, то проверяется наличие этого символа во второй строке и возвращается 1 или 0, в первом случае к LCS добавляется этот символ.

С учетом отсутствия необходимости хранить матрицы полностью требования к памяти оцениваются как O(|x| + |y|), временная сложность по-прежнему O(|x||y|).

2.5.4. АЛГОРИТМ ХАНТА–ШИМАНСКОГО

В алгоритме, который в 1977 г. опубликовали Джеймс Хант и Томас Шиманский [18], используется так называемый «массив пороговых значений». При описании этого алгоритма значительно удобнее счи-

тать, что индекс первого символа строки равен 1. Элемент массива пороговых значений с индексами i, s содержит минимальный индекс j, при котором строки x[1..i] и y[1..j] имеют LCS длины s. Пусть этот массив обозначен k, тогда можно сказать, что ki,s – это длина самого короткого префикса y, имеющего с префиксом x длины i LCS длины s. Если для некоторых i и s таких j не существует, то считается что ki,s не определено. Если x = ‘aebcde’ и y = ‘facbfacfb’, то k5,1 = 2, k5,2 = 4,

k5,3 = 6, а k5,4 и k5,5 не определены. Неопределенные ki,s заполняются значением (|y| + 1).

Прежде чем перейти к алгоритму, необходимо рассмотреть некоторые свойства массива пороговых значений. Поскольку j – это минимальный индекс, символ y[ki,s] будет последним членом LCS, следова-

тельно, элементы ki,1,..,ki,p (p < (|y| + 1)) образуют возрастающую по-

следовательность. Пусть префиксу строки x длиной i для достижения длины LCS, равной s, был сопоставлен префикс y длиной ki,s. Если расширить префикс x на один символ, то для достижения такой же

60

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]