Длинной общей подпоследовательности 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".

3 ответов


просто обобщите рекуррентное отношение.

три строки:

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

должно быть легко обобщить на большее количество строк из этого.


чтобы найти самую длинную общую подпоследовательность (LCS) из 2 строк A и B, вы можете пересечь 2-мерный массив по диагонали, как показано в ссылке, которую вы опубликовали. Каждый элемент в массиве соответствует задаче нахождения LCS подстрок A' и B' (A вырезается по номеру строки, B вырезается по номеру столбца). Эта проблема может быть решена путем вычисления значения всех элементов массива. Вы должны быть уверены, что при вычислении значения элемента массива, все подпроблемы требуется рассчитать, что данное значение уже решено. Вот почему вы пересекаете 2-мерный массив по диагонали.

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

вместо итерации n-мерного массива в специальном порядке вы может также решить проблему рекурсивно. При рекурсии важно сохранить промежуточные решения, так как для многих ветвей потребуются одни и те же промежуточные решения. Я написал небольшой пример на C#, который делает это:

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"


Я просто должен был сделать это для домашней работы, так что вот мое решение динамического программирования в 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]