среда, 16 марта 2011 г.

Построение наибольшей общей подпоследовательности (LCS) на основе наибольшей возрастающей подпоследовательности (LIS)

[Все алгоритмы нахождения LCS]

Данный алгоритм относится к разряду “быстрых” алгоритмов, которые находят LCS двух строк. В среднем случае время работы O(n*log(h)), но в худшем случае все такие “быстрые” алгоритмы вырождаются в O(n*n*log(h)).

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

А теперь по пунктам остановимся на основных моментах.

1) Путь П – это набор из n не обязательно различных целых чисел.

2) Increasing subsequence (IS) of П (Возрастающая подпоследовательность последовательности П) – это набор чисел из П, которые строго увеличиваются при прохождении слева направо.
Пример: 
П = (5,3,4,4,9,6,2,1,8,7,10). 
IS = (3,4,6,8,10), (5,9,10) 

3) Longest increasing subsequence(LIS) of П (Наибольшая возрастающая подпоследовательность последовательности П) – это возрастающая подпоследовательность последовательности П, которая имеет максимальную длину.

4) Decreasing subsequence (DS) of П – невозрастающая подпоследовательность последовательности П при проходе слева направо.
Пример: DS = (5,4,4,2,1)

5) Cover(Покрытие) – множество непересекающихся DS последовательности П, которое содержит все элементы П. Размер покрытия равен количеству DS в покрытии. 
Пример: 
П = (5,3,4,9,6,2,1,8,7)
cover = (5,3,2,1), (4), (9,6), (8,7)

6) Smallest cover (SC) (Наименьшее покрытие) – это покрытие минимального размера

7) Лемма.
Если I – это IS последовательности П, длина которой равна размеру покрытия С последовательности П, тогда I – это LIS последовательности П, а C – SC.
Доказательство леммы смотрите в статье

8) Если П = (5,3,4,9,6,2,1,8,7,10), тогда
D1 = (5,3,2,1),
D2 = (4),
D3 = (9,6),
D4 = (8,7),
D5 = (10),
где D1..D5 – это DS последовательности П, образующие SC.

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

9) Лобовая реализация построения SC будет иметь сложность O(N*N). Такая ситуация может возникнуть, если исходная последовательность П является IS.

Сейчас рассмотрим алго со сложностью O(N*log(N)).
П = (5,3,4,9,6,2,1,8,7)

i

П[i]

Хвосты DS
(значение хвоста, номер DS)

DS

1 5 (5,1) D1=(5)
2 3 (3,1) D1=(5,3)
3 4 (3,1),(4,2) D1=(5,3)     D2=(4)
4 9 (3,1), (4,2), (9,3) D1=(5,3)     D2=(4) D3=(9)
5 6 (3,1), (4,2), (6,3) D1=(5,3)     D2=(4) D3=(9,6)
6 2 (2,1), (4,2), (6,3) D1=(5,3,2)   D2=(4) D3=(9,6)
7 1 (1,1), (4,2), (6,3) D1=(5,3,2,1) D2=(4) D3=(9,6)
8 8 (1,1), (4,2), (6,3), (8,4) D1=(5,3,2,1) D2=(4) D3=(9,6) D4=(8)
9 7 (1,1), (4,2), (6,3), (7,4) D1=(5,3,2,1) D2=(4) D3=(9,6) D4=(8,7)


Можно заметить что значения хвостов DS представляют собой IS, поэтому при добавлении нового элемента П[i] номер DS, к которому он относится можно не линейно а за O(log(N)), для этого можно использовать модификацию бинарного поиска(lower_bound)

10) Плавно переходим к исходной задаче.
Пусть S1 = “abacx” и S2 = “baabca”.
r(i) – количество вхождений символа S1[i] в строку S2
Тогда:
r(1) = 3, r(2) = 2, r(3) = 3, r(4) = 1, r(5) = 0.
11) Пусть list(x) – список позиций символа x из строки S1 в строке S2 в порядке убывания. Т.е.
list(a) = (6,3,2)
list(b) = (4,1)
list(c) = (5)
list(x) = (empty)

12) Пусть П(S1,S2) – последовательность, полученная путем конкатенации списков list(S1[i]), где i = 1..n.
Для рассматриваемого примера
П(S1, S2) = (6,3,2,4,1,6,3,2,5)

13) Для получения LCS(S1,S2) необходимо найти LIS(П(S1,S2)) из п.12. Элементы, попавшие в этот LIS соответствуют номерам элементов в S2, образующих LCS(S1, S2).

Пример:
LIS = (1,2,5), LCS = bac
LIS = (3,4,6), LCS = aba


Демонстрационное решение: здесь

вторник, 15 марта 2011 г.

Поиск наибольшей общей подпоследовательности(LCS) с помощью динамического программирования

[Все алгоритмы нахождения LCS]
Очень простой и понятный алгоритм, основанный на двумерном ДП.
Сложность по памяти и времени O(n*m).

Рассмотрим две строчки: A = “bedcadb” и B = “abcdede”. 
1) Буквы строчки A напишем перед строками матрицы, а элементы строчки B над столбцами матрицы.
2) Матрица будет иметь нулевые столбец и строку, состоящие из нулей.
3) Саму матрицу давайте назовем L.
4) В элементе матрицы L[i][j] будет хранится длина наибольшей общей подпоследовательности для префиксов A[1..i] и B[1..j].
Из п.4 делаем вывод, что нумерация букв в строках с единицы.
Префикс A[1..i] – это первые i символов строки A.

Для начала давайте заполним матрицу L по этим 4 принципам:

Разберем элемент L[3][4]. В данном случае рассматриваются два префикса: “bed” и “abcd”. Чисто интуитивно можно догадаться, что lcs для этих двух строк будет “bd”, поэтому в самом элементе храним 2. Все остальные элементы можно заполнить интуитивно.

Вот как раз на этом моменте выключаем интуицию(в рамках разумного) и начинаем искать “научный” принцип заполнения этой матрицы.

В ходе Ваших рассуждений должны появится следующие рекуррентные формулы:
             
           | L[i-1][j-1] + 1, если a[i] == b[j]
L[i][j] = < 
           | max(L[i-1][j],L[i][j-1]), иначе
Результирующая длина lcs для полных строк a и b находится в элементе L[7][7].

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

Оранжевая lcs: bed
Желтая    lcs: bcd
Затем тренируемся на задачах:
1) Найти длину LCS
2) По матрице восстановить саму LCS

Демонстрационное решение: здесь

Наибольшая общая подпоследовательность (LCS)

Для упрощения длины обоих строк одинаковые.

* Алгоритм Нудельмана-Вунша – O(n*n)

* Реализация LCS на основе LIS(Наибольшая возрастающая подпоследовательность) – O(n*n*log(h), в среднем O(n*log(h))

* Алгоритм Эллисона-Дикса – O(n*n/32) 

обозначения:
n – длина строки,
h – количество общих буквенных пар. Т.е. если в первой строке есть 3, а во второй - 2 буквы ‘a’, то количество общих буквенных пар для ‘a’ равно 6.

понедельник, 28 февраля 2011 г.

Буквометика. Метод ветвей и границ.

Данный пост навеян главой “2.8.3. Расшифровка криптограм” из книги В.Д. Колдаева “Основы алгоритмизации и программирования”.

Напомним, что о теме Буквометики говорилось
ранее.
Вкратце напомню, что суть задачи в решении ребуса, подобному этому:
                           MOTOR 
                     +      FIAT  
                          ------
                           VOLVO
где вместо букв нужно подставить цифры, при этом две различные буквы должны иметь различный цифровой эквивалент.

Описание:
В предыдущем посте перебирались все размещения без повторений, после чего проверялась корректность получившегося набора.

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

Рассмотрим текущий ребус.
Представим ребус в виде матрицы из трех строк, и столбцов, количество которых равно длине суммы. В первой строке хранится первое слагаемое, во второй – второе, в третье – сумма. Если имеется ведущий ноль(как например в FIAT), то храним ведущий пробел на первом месте.
Учитывая все вышесказанное, получаем матрицу
                             01234
                         0 [ MOTOR ]
                         1 [ _FIAT ]
                         2 [ VOLVO ]
символ ‘_’ соответствует пробелу.

Будем формировать правила при проходе по матрице сверху вниз, слева направо.
1) На букву M накладывается только одно ограничение – оно не может быть равным 0, и следовательно может принимать любое значение из интервала. [1,9]
2) Буква V однозначно получается из значения M и переноса, соответствующего 0-ому столбцу, который определяется из суммы O+F.
Следовательно если перенос(p[0]) равен нулю, тогда
2.1) V = M и O + F < 10
Если p[0] = 1, тогда
2.2) V = M + 1 и O + F >=10.

Здесь необходимо учесть тот факт, что на сумму O + F будет еще влиять перенос на первом столбце, поэтому правильно будет так:
2.1) V = M     и O + F + p[1] < 10
2.2) V = M + 1 и O + F + p[1] >=10.
3) Буква O может принимать любые значения из интервала [0,9]
4) Буква F будет вычисляться из неравенства 2.1) или 2.2) в зависимости от значения переноса p[0] и равенства O+F=O
И т.д. до конца. Если на каком то этапе получается, что текущая буква не может иметь ни одного значения, значить необходимо поменять значения ранее стоящих букв, или значения переносов.

Реализация:
Удобно решать рекурсивно. При этом сама рекурсия будет иметь прототип: void rec(int row, int col, int prevCarry, int carry), где row и col – номера строки и столбца текущего элемента в матрице, а prevCarry и carry – значения переноса на предыдущем и  текущем разрядах.

Правила, образующие неравенства удобно хранить в виде структуры:

  1. struct rule {
  2. // this + pair < 10, если prevCarry == 0
  3. // this + pair >=10, если prevCarry == 1
  4.   char pair;     // парный символ   
  5.   int prevCarry; // предыдущий перенос
  6.   int carry;     // текущий перенос
  7. };
* This source code was highlighted with Source Code Highlighter.

А набор правил в виде рванной матрицы:
vector<vector<rule> > rules(256);
где значение буквы this определяется номер строки в рванной матрице правил.
Например рассмотрим правило, где фигирируют буквы A,O. prevCarry = 1, carry = 1.
Из представленных значений ясно, что A + O + carry >= 10. Правило занесем таким способом: 
rules[‘A’].push_back(rule(‘0’,prevCarry,carry));

[Решение]

P.S: К сожалению не удалось зачесть эту реализацию на задаче 251.Футбол и математика из-за Memory Limit. Но от этого данный подход не теряет своей привлекательности по быстродействию.

вторник, 15 февраля 2011 г.

Часть 5. Буквометика

[Все темы по перестановкам]

Литература: Кнут. Том 4. Выпуск 2. Генерация всех картежей и перестановок. 2008

Задача: Футбол и математика

Описание решение:

Суть задачи решить ребус аналогичный такому:
                        MOTOR
                  +      FIAT 
                       ------
                        VOLVO
Где вместо букв нужно поставить цифру. При этом разные буквы должны иметь разные цифровые значения.
По условию задачи количество различных букв не превосходит 10.
Значит в общем случае нужно перебрать все размещения без повторений для n=10(10 цифр) и k = количеству различных букв в ребусе. А в худшем случае – 10! различных перестановок.
Генерация размещений без повторений имеет схожую природу с генерацией перестановок без повторений. Поэтому данная задача попала в блок к перестановкам.

Алгоритм будет иметь следующую структуру:
1) Получение строки, содержащей все различные буквы исходного ребуса в порядке слева направо, сверху вниз. Для примера это будет строка “MOTRFIAVL”.
1) Генерация текущего размещения без повторений для заданных n и k в лексикографическом порядке.
2) Т.к. длина текущего размещения равна длине строке различных букв из п.1), проинициализировать i-ую букву i-ым значением размещения.
3) Проверка корректности ребуса.
4) Если возможно, перейти на п. 1).

Реализация:
В самом решении использовалась рекурсивная схема, схожая со схемой генерации всех перестановок без повторений.
Обратите внимание на изящную реализацию считывания(ст.4):

  1. void input()
  2. {
  3.   char a[12],b[12],c[12];
  4.   scanf("%[A-Z]+%[A-Z]=%[A-Z]",&a,&b,&c);
  5.   A = string(a); B = string(b); C = string(c);
  6. }
* This source code was highlighted with Source Code Highlighter.

[Решение]

P.S: Метод ветвей и границ для данной задачи.