Самая длинная общая подпоследовательность строк 3+
Я пытаюсь найти самую длинную общую подпоследовательность из 3 или более строк. В статье в Википедии есть отличное описание как это сделать для 2 строк, но я немного не уверен, как расширить это до 3 или больше строк.
Существует множество библиотек для поиска LCS из 2 строк, поэтому я хотел бы использовать один из них, если это возможно. Если у меня есть 3 строки A, B и C, правильно ли найти LCS A и B как X, а затем найти LCS X и C, или это неправильный способ сделать это?
Я реализовал его в Python следующим образом:
import difflib
def lcs(str1, str2):
sm = difflib.SequenceMatcher()
sm.set_seqs(str1, str2)
matching_blocks = [str1[m.a:m.a+m.size] for m in sm.get_matching_blocks()]
return "".join(matching_blocks)
print reduce(lcs, ['abacbdab', 'bdcaba', 'cbacaa'])
Это выводит "ba", однако оно должно быть "baa".
Ответы
Ответ 1
Просто обобщите рекуррентное соотношение.
Для трех строк:
dp[i, j, k] = 1 + dp[i - 1, j - 1, k - 1] if A[i] = B[j] = C[k]
max(dp[i - 1, j, k], dp[i, j - 1, k], dp[i, j, k - 1]) otherwise
Следует легко обобщать на большее количество строк из этого.
Ответ 2
Чтобы найти самую длинную общую подпоследовательность (LCS) из двух строк A и B, вы можете пересечь двумерный массив по диагонали, как показано в ссылке, которую вы опубликовали. Каждому элементу массива соответствует проблема нахождения LCS подстрок A 'и B' (A, вырезанных номером строки, B, вырезанным по его номеру столбца). Эта проблема может быть решена путем вычисления значения всех элементов в массиве. Вы должны быть уверены, что при вычислении значения элемента массива все под-проблемы, необходимые для вычисления данного значения, уже решены. Вот почему вы пересекаете двумерный массив по диагонали.
Это решение можно масштабировать до нахождения самой длинной общей подпоследовательности между N строками, но для этого требуется общий способ итерации массива из N измерений, так что любой элемент достигается только тогда, когда все подзадачи, которые элемент требует решения, имеют была решена.
Вместо повторения N-мерного массива в специальном порядке вы также можете решить проблему рекурсивно. При рекурсии важно сохранить промежуточные решения, так как для многих отраслей потребуются те же промежуточные решения. Я написал небольшой пример в С#, который делает это:
string lcs(string[] strings)
{
if (strings.Length == 0)
return "";
if (strings.Length == 1)
return strings[0];
int max = -1;
int cacheSize = 1;
for (int i = 0; i < strings.Length; i++)
{
cacheSize *= strings[i].Length;
if (strings[i].Length > max)
max = strings[i].Length;
}
string[] cache = new string[cacheSize];
int[] indexes = new int[strings.Length];
for (int i = 0; i < indexes.Length; i++)
indexes[i] = strings[i].Length - 1;
return lcsBack(strings, indexes, cache);
}
string lcsBack(string[] strings, int[] indexes, string[] cache)
{
for (int i = 0; i < indexes.Length; i++ )
if (indexes[i] == -1)
return "";
bool match = true;
for (int i = 1; i < indexes.Length; i++)
{
if (strings[0][indexes[0]] != strings[i][indexes[i]])
{
match = false;
break;
}
}
if (match)
{
int[] newIndexes = new int[indexes.Length];
for (int i = 0; i < indexes.Length; i++)
newIndexes[i] = indexes[i] - 1;
string result = lcsBack(strings, newIndexes, cache) + strings[0][indexes[0]];
cache[calcCachePos(indexes, strings)] = result;
return result;
}
else
{
string[] subStrings = new string[strings.Length];
for (int i = 0; i < strings.Length; i++)
{
if (indexes[i] <= 0)
subStrings[i] = "";
else
{
int[] newIndexes = new int[indexes.Length];
for (int j = 0; j < indexes.Length; j++)
newIndexes[j] = indexes[j];
newIndexes[i]--;
int cachePos = calcCachePos(newIndexes, strings);
if (cache[cachePos] == null)
subStrings[i] = lcsBack(strings, newIndexes, cache);
else
subStrings[i] = cache[cachePos];
}
}
string longestString = "";
int longestLength = 0;
for (int i = 0; i < subStrings.Length; i++)
{
if (subStrings[i].Length > longestLength)
{
longestString = subStrings[i];
longestLength = longestString.Length;
}
}
cache[calcCachePos(indexes, strings)] = longestString;
return longestString;
}
}
int calcCachePos(int[] indexes, string[] strings)
{
int factor = 1;
int pos = 0;
for (int i = 0; i < indexes.Length; i++)
{
pos += indexes[i] * factor;
factor *= strings[i].Length;
}
return pos;
}
Мой пример кода может быть оптимизирован далее. Многие из кэшируемых строк - это дубликаты, а некоторые дубликаты с добавлением только одного добавленного символа. Это занимает больше места, чем необходимо, когда входные строки становятся большими.
На входе: "666222054263314443712", "5432127413542377777", "6664664565464057425"
Возвращенный LCS - "54442"
Ответ 3
Мне просто нужно было сделать это для домашней работы, так что вот мое решение для динамического программирования на python довольно эффективно. Это O (nml), где n, m и l - длины трех последовательностей.
Решение работает, создавая 3D-массив, а затем перечисляя все три последовательности для вычисления пути самой длинной подпоследовательности. Затем вы можете вернуться через массив, чтобы восстановить фактическую подпоследовательность из своего пути.
Итак, вы инициализируете массив ко всем нулям, а затем перечисляете три последовательности. На каждом шаге перечисления вы либо добавляете один к длине самой длинной подпоследовательности (если есть совпадение), либо просто переносите самую длинную подпоследовательность с предыдущего шага перечисления.
Как только перечисление будет завершено, вы можете теперь проследить массив, чтобы восстановить подпоследовательность с шагов, которые вы предприняли. то есть при перемещении назад от последней записи в массиве каждый раз, когда вы сталкиваетесь с совпадением, вы просматриваете его в любой последовательности (используя координату из массива) и добавляете ее в подпоследовательность.
def lcs3(a, b, c):
m = len(a)
l = len(b)
n = len(c)
subs = [[[0 for k in range(n+1)] for j in range(l+1)] for i in range(m+1)]
for i, x in enumerate(a):
for j, y in enumerate(b):
for k, z in enumerate(c):
if x == y and y == z:
subs[i+1][j+1][k+1] = subs[i][j][k] + 1
else:
subs[i+1][j+1][k+1] = max(subs[i+1][j+1][k],
subs[i][j+1][k+1],
subs[i+1][j][k+1])
# return subs[-1][-1][-1] #if you only need the length of the lcs
lcs = ""
while m > 0 and l > 0 and n > 0:
step = subs[m][l][n]
if step == subs[m-1][l][n]:
m -= 1
elif step == subs[m][l-1][n]:
l -= 1
elif step == subs[m][l][n-1]:
n -= 1
else:
lcs += str(a[m-1])
m -= 1
l -= 1
n -= 1
return lcs[::-1]
Ответ 4
Этот код ниже может найти самую длинную общую подпоследовательность в N строках. Это использует itertools для генерации требуемых комбинаций индексов, а затем использует эти индексы для поиска общей подстроки.
Пример выполнения:
Входные данные :
Введите количество последовательностей: 3
Введите последовательность 1: 83217
Введите последовательность 2: 8213897
Введите последовательность 3: 683147
Выход:
+837
from itertools import product
import numpy as np
import pdb
def neighbors(index):
N = len(index)
for relative_index in product((0, -1), repeat=N):
if not all(i == 0 for i in relative_index):
yield tuple(i + i_rel for i, i_rel in zip(index, relative_index))
def longestCommonSubSequenceOfN(sqs):
numberOfSequences = len(sqs);
lengths = np.array([len(sequence) for sequence in sqs]);
incrLengths = lengths + 1;
lengths = tuple(lengths);
inverseDistances = np.zeros(incrLengths);
ranges = [tuple(range(1, length+1)) for length in lengths[::-1]];
for tupleIndex in product(*ranges):
tupleIndex = tupleIndex[::-1];
neighborIndexes = list(neighbors(tupleIndex));
operationsWithMisMatch = np.array([]);
for neighborIndex in neighborIndexes:
operationsWithMisMatch = np.append(operationsWithMisMatch, inverseDistances[neighborIndex]);
operationsWithMatch = np.copy(operationsWithMisMatch);
operationsWithMatch[-1] = operationsWithMatch[-1] + 1;
chars = [sqs[i][neighborIndexes[-1][i]] for i in range(numberOfSequences)];
if(all(elem == chars[0] for elem in chars)):
inverseDistances[tupleIndex] = max(operationsWithMatch);
else:
inverseDistances[tupleIndex] = max(operationsWithMisMatch);
# pdb.set_trace();
subString = "";
mainTupleIndex = lengths;
while(all(ind > 0 for ind in mainTupleIndex)):
neighborsIndexes = list(neighbors(mainTupleIndex));
anyOperation = False;
for tupleIndex in neighborsIndexes:
current = inverseDistances[mainTupleIndex];
if(current == inverseDistances[tupleIndex]):
mainTupleIndex = tupleIndex;
anyOperation = True;
break;
if(not anyOperation):
subString += str(sqs[0][mainTupleIndex[0] - 1]);
mainTupleIndex = neighborsIndexes[-1];
return subString[::-1];
numberOfSequences = int(input("Enter the number of sequences: "));
sequences = [input("Enter sequence {} : ".format(i)) for i in range(1, numberOfSequences + 1)];
print(longestCommonSubSequenceOfN(sequences));