Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Динамическое программирование.doc
Скачиваний:
25
Добавлен:
05.11.2018
Размер:
1.1 Mб
Скачать

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. Если zkxm, то мы можем дописать xт = yn в конец последовательности Z и получить общую подпоследовательность длины k + 1, что противоречит условию. Стало быть, zk = xm-1 = yn. Если у последовательностей Xm-1 и Yn-1 есть более длинная (чем Zk-1) общая подпоследовательность, то мы можем дописать к ней xm =yn и получить общую подпоследовательность для Х и У, более длинную, чем Z, чего быть не может.

2. Коль скоро zkxm последовательность 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. Если же xmyn, то надо решить две подзадачи: найти НОП для 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)) не удаётся.