
3. Наибольшая общая подпоследовательность
Подпоследовательность получается из данной последовательности, если удалить некоторые её элементы (сама последовательность также считается своей подпоследовательностью). Формально: последовательность Z=(z1,z2,...,zn) называется подпоследовательностью (subsequence) последовательности Х=(x1,x2,...,xn), если существует строго возрастающая последовательность из индексов (i1,i2,...,ik), для которой zj=xi при всех j=1,2,..,k. Например, Z=(В, С, D, В) является подпоследовательностью последовательности Х=(A, В, С, В, D, A, В), соответствующая последовательность индексов ecть (2,3,5,7). (Отметим, что говоря о последовательностях, мы- в отличием курсов математического анализа- имеем в виду конечные последовательности).
Будем говорить, что последовательность Z является общей подпоследовательностыо (common subsequence) последовательностей Х и Y, если Z является подпоследовательностью как Х, так и Y. Пример: Х=(А, В, C, В, D, A, B), Y = (В, D,C, A, В, A), Z= (В, С, A). Последовательность Z в этом примере- не самая длинная из общих подпоследовательностей Х и Y (последовательность (В, С, В, А) длиннее). Последовательность (В, С, В, А) будет наибольшей обшей подпоследовательностью для X и Y, поскольку общих подпоследователыюстей длины 5 у них нет. Наибольших общих подпоследовательностей может быть несколько. Например, (В, D, А, В) — другая наибольшая общая подпоследовательность Х и Y.
Задача о наибольшей общей подпоследовательности (сокращенно НОП; по-английски LCS = longest-common-subsequence) состоит в том, чтобы найти общую подпоследовательность наибольшей длины для двух данных последовательностей Х и Y. В этом разделе мы покажем, как решить эту задачу с помощью динамического программирования.
Строение наибольшей общей подпоследовательности
Если решать задачу о НОП «в лоб», перебирая все подпоследовательности последовательности Х и проверяя для каждой из них, не будет ли она подпоследовательностью последовательности Y, то алгоритм будет работать экспоненциальное время, поскольку последовательность длины т имеет 2m подпоследовательностей (столько же, сколько подмножеств у множества {1, 2,.. ,m}).
Однако задача о НОП обладает свойством оптимальности для подзадач, как показывает теорема 1 (см. ниже). Подходящее множество подзадач- множество пар префиксов двух данных последовательностей. Пусть Х= (x1,x2,...,xm)- некоторая последовательность. Её префикс (prefix) длины i- это последовательность Хi =(x1,x2,...,xi) (при i от 0 до m). Например, если Х =(А, В, С, В, D, А, В), то Х4 = (А, В, С, В), а X0 - пустая последовательность.
Теорема 1 (о строении НОП). Пусть Z=(z1,z2,...,zk) — одна из наибольших общих подпоследовательностей для Х = (x1,x2,...,xm) и У = (у1,y2,...,yn). Тогда:
1) если xт = yn, то zk = xm =yn и Zk-1 является НОП для Xm-1 и Yn-1
2) если xm≠yn и zk≠xm , то Z является НОП для Хт-1 и Y;
3) если xm≠yn и zk≠yn , то Z является НОП для Хт и Yn-1
Доказательство
1. Если zk≠xm, то мы можем дописать xт = yn в конец последовательности Z и получить общую подпоследовательность длины k + 1, что противоречит условию. Стало быть, zk = xm-1 = yn. Если у последовательностей Xm-1 и Yn-1 есть более длинная (чем Zk-1) общая подпоследовательность, то мы можем дописать к ней xm =yn и получить общую подпоследовательность для Х и У, более длинную, чем Z, чего быть не может.
2. Коль скоро zk≠xm последовательность Z является общей подпоследовательностью для Хm-1 и Y. Так как Z— НОП для Х и Y, то она тем более является НОП для Хm-1 и Y.
3. Аналогично 2.
Мы видим, что НОП двух последовательностей содержит в себе наибольшую общую подпоследовательность их префиксов. Стало быть, задача о НОП обладает свойством оптимальности для подзадач. Сейчас мы убедимся, что перекрытие подзадач также имеет место.
Рекуррентная формула
Теорема 16.1 показывает, что нахождение НОП последовательностей X=(x1,x2,...,xm) и Y=(y1,y2,...,yn) сводится к решению либо одной, либо двух подзадач. Если xm = yn , то достаточно найти НОП последовательностей Xm-1. и Yn-1 и дописать к ней в конце xm=yn. Если же xm≠yn, то надо решить две подзадачи: найти НОП для Xm-1 и Y, а затем найти НОП для Х и Yn-1. Более длинная из них и будет служить НОП для Х и Y.
Теперь сразу видно, что возникает перекрытие подзадач. Действительно, чтобы найти НОП Х и Y, нам может понадобиться найти НОП Хm-1 и Y, а также НОП Х и Yn-1; каждая из этих задач содержит подзадачу нахождения НОП для Хm-1 и Yn-1. Аналогичные перекрытия будут встречаться и далее.
Как и в задаче перемножения последовательности матриц, мы начнём с рекуррентного соотношения для стоимости оптимального решения. Пусть c[i,j], обозначает длину НОП для последовательностей Xi и Yj. Если i или j равны нулю, то одна из двух последовательностей пуста, так что c[i,j]= 0. Сказанное выше можно записать так:
(5)
Вычисление длины НОП
Исходя из соотношения (5), легко написать рекурсивный алгоритм работающий экспоненциальное время и вычисляющий длину НОП двух данных последовательностей. Но поскольку различных подзадач всего 0(mn), лучше воспользоваться динамическим программированием.
float x[t],y[r];
double LCS_Length(float x[], float y[]);
{m=t;
n=r;
for (i=1;i<=m;i++)
{c[i,0]=0};
for (j=0;j<=n;j++)
{c[0,j]=0};
for (i=1;i<=m;i++)
{for (j=1;j<=n;j++)
{if (x[i]=y[j])
{c[i][j]=c[i-1][j-1]+1;
cout<<"b[i][j]=^\"}
else {if (c[i-1][j]>=c[i-1][j-1]+1;
cout<<"b[i][j]=^|"}
else {c[i][j]=c[i][j-1];
cout<<"b[i][j]=<-";}}}
return c,b;
}
На рис.3 показана работа lcs-length для X=(A,B,C,B,D,A,B) и Y= (B,D,C,A,B,A). Алгоритм lcs-length требует времени 0(mn): на каждую клетку требуется 0(1) шагов.
Рис. 3. Таблицы c и b, созданные алгоритмом lcs-length при Х=(А, В,С, B, D, А, В) и Y =(В, D, С, A, В, A). В клетке с координатами (i,j) записаны число с[i,j] и стрелка b[i,j]. Число 4 в правой нижней клетке есть длина НОП. При i,j > 0 значение c[i,j] определяется тем, равны ли xi и уj, и вычисленными ранее значениями c[i-1,j], c[i,j- 1] и c[i-l,j-l]. Путь по стрелкам, ведущий из с[7,6], заштрихован. Каждая косая стрелка на этом пути соответствует элементу НОП (эти элементы выделены).
Улучшение алгоритма
После того, как алгоритм разработан, нередко удаётся сделать его более экономным. В нашем примере можно обойтись без таблицы b. В самом деле каждое из чисел c[i,j] зависит от c[i-1,j], c[i,j-1] и с[i-1,j-1]. Зная c[i,j] мы можем за время 0(1) выяснить, какая из этих трёх записей использовалась. Тем самым можно найти НОП за время 0(т+п) с помощью одной только таблицы с. При этом мы экономим 0(mn) памяти. (Впрочем, асимптотика не меняется: объём таблицы c есть также 0(mn).)
Если нас интересует только длина наибольшей общей подпоследовательности, то столько памяти не нужно: вычисление c[i,j] затрагивает только две строки с номерами i и i-1 (это не предел экономии: можно обойтись памятью на одну строку таблицы с плюс ещё чуть-чуть). При этом, однако, саму подпоследовательность найти (за время 0(m+n)) не удаётся.