Поиск самой длинной подпоследовательности палиндрома с меньшим объемом памяти

Я пытаюсь решить проблему динамического программирования из Cormem's введение в алгоритмы 3-е издание (стр. 405), который пишет следующее:

палиндром-это непустая строка некоторые алфавит, читает то же вперед и назад. Примеры палиндромы-это все строки длины 1, civic, racecar и aibohphobia (боязнь палиндромов).

дайте эффективный алгоритм для поиска самый длинный палиндром, который это подпоследовательность заданной входной строки. Например, учитывая входные данные character, ваш алгоритм должен возвращение carac.

Ну, я мог бы решить это двумя способами:

первый вариант:

самая длинная подпоследовательность палиндрома (LPS) строки-это просто Самая Длинная Общая Подпоследовательность себя и своего обратного. (Я построил это решение после решения другого связанного вопроса, который задает Самая Длинная Возрастающая Подпоследовательность последовательности). Поскольку это просто вариант LCS, он также занимает O(n2) время и o(n2) память.

второй вариант:

второе решение немного более разработано, но также следует общему шаблону LCS. Это происходит из следующего повторения:

lps(s[i..j]) = 
    s[i] + lps(s[i+1]..[j-1]) + s[j], if s[i] == s[j];
    max(lps(s[i+1..j]), lps(s[i..j-1])) otherwise

псевдокод для вычисления длины lps является следующим:

compute-lps(s, n):

    // palindromes with length 1
    for i = 1 to n:
        c[i, i] = 1
    // palindromes with length up to 2
    for i = 1 to n-1:
        c[i, i+1] = (s[i] == s[i+1]) ? 2 : 1

    // palindromes with length up to j+1
    for j = 2 to n-1:
        for i = 1 to n-i:
            if s[i] == s[i+j]:
                c[i, i+j] = 2 + c[i+1, i+j-1]
            else:
                c[i, i+j] = max( c[i+1, i+j] , c[i, i+j-1] )

это все еще занимает O (n2) время и память, если я хочу эффективно построить lps (потому что мне понадобятся все ячейки на столе). Анализируя связанные проблемы, такие как LIS, которые могут быть решены с помощью подходов, отличных от LCS-подобных с меньшим объемом памяти (LIS разрешима с O(n) памятью), мне было интересно, можно ли решить его с O(n) памятью.

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

2 ответов


вот очень эффективная версия памяти. Но я не доказал, что это так!--20-->всегда O(n) память. (С предварительной обработки он может лучше, чем O(n2) CPU, хотя O(n2) это в худшем случае.)

начните с самого левого положения. Для каждой позиции следите за таблицей самых дальних точек, в которых вы можете генерировать отраженные подпоследовательности длины 1, 2, 3 и т. д. (Это означает, что подпоследовательность слева от нашей точки отражается в право.) Для каждой отраженной подпоследовательности мы храним указатель на следующую часть подпоследовательности.

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

давайте рассмотрим это для character.

  1. мы начинаем с нашего лучшего палиндрома, являющегося буквой "c", и наша зеркальная подпоследовательность достигается с парой (0, 11) которые находятся вне концов строки.
  2. Далее рассмотрим "c" в позиции 1. Наши лучшие зеркальные подпоследовательности в виде (length, end, start) теперь [(0, 11, 0), (1, 6, 1)]. (Я оставлю связанный список, который вам нужно создать, чтобы найти палиндром.
  3. Далее рассмотрим!--7--> в положение 2. Мы не улучшаем границы [(0, 11, 0), (1, 6, 1)].
  4. Далее рассмотрим!--9--> на позиции 3. Мы улучшаем границы до [(0, 11, 0), (1, 6, 1), (2, 5, 3)].
  5. Далее рассмотрим!--11--> в позиции 4. Мы улучшаем границы до [(0, 11, 0), (1, 10, 4), (2, 5, 3)]. (Здесь был бы полезен связанный список.

работая через остальную часть списка, мы не улучшаем этот набор границ.

так мы с самой длинной зеркальной список длины 2. И мы будем следовать связанному списку (который я не записывал в этом описании, чтобы найти это ac. Поскольку концы этого списка находятся на позициях (5, 3) мы можем перевернуть список, вставить символ 4, затем добавьте список, чтобы получить carac.

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

при классическом компромиссе памяти / процессора вы можете предварительно обработать список один раз во времени O(n) для создания O(n) размер хэша массивов, где появляются определенные элементы последовательности. Это может позволить вам сканировать "улучшить зеркальную подпоследовательность с этим сопряжением", не рассматривая всю строку, которая обычно должна быть значительной экономией на CPU для более длинных строк.


первое решение в вопросе @Luiz Rodrigo неверно: самый длинный общий подзапрос (LCS) строки и ее обратный не обязательно является палиндромом.

пример: для строки CBACB CAB-это LCS строки и ее реверса, и это, очевидно, не палиндром. Однако есть способ заставить его работать. После построения LCS строки и ее реверса возьмите левую ее половину (включая средний символ для строк нечетной длины) и дополните ее справа обратной левой половиной (не включая средний символ, если длина строки нечетная). Очевидно, что это будет палиндром, и можно тривиально доказать, что это будет подпоследовательность строки.

для выше LCS палиндром, построенный таким образом, будет CAC.