Как найти максимальный палиндром в строке

Алгоритм поиска самой длинной подстроки-палиндрома

Время на прочтение
5 мин

Количество просмотров 14K

Один из самых прекрасных алгоритмов в информатике, который показывает, как можно получить большое ускорение от «вялого» O(n3) до молниеносного1 O(n), просто посмотрев на проблему с другой точки зрения.

Задача состоит в том, чтобы найти самую длинную подстроку, которая является палиндромом (читается одинаково слева направо и справа налево, например, «racecar»). Так, самый длинный палиндром в строке «Fractions are never odd or even» это «never odd or even» (регистр букв и пробелы игнорируются). Это также имеет практическое применение в биохимии (ГААТТЦ или ЦТТААГ являются палиндромными последовательностями2). К тому же, эту задачу3 часто дают на собеседовании.

Самый простой и прямой подход (при этом самый медленный) — перебирать с начала все подстроки всех длин и проверять, является ли текущая палиндромом:

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

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

Псевдокод такого решения

ЦИКЛ по символам всей строки:
	ЦИКЛ по всем длинам, начиная с текущего символа:
		ЦИКЛ по символам в этой подстроке:

явно указывает, что метод со сложностью O(n3) (где n — это длина начальной строки) является быстровозрастающей функцией.

Если бы вместо перебора с самых начал подстрок, мы начинали перебор с середины, это позволило бы нам использовать результаты, которые мы получили на предыдущих шагах.

Например, если мы знаем, что «eve» — это палиндром, то нам потребуется всего одно сравнение, чтобы выяснить, что «level» тоже палиндром. В первом решении нам бы пришлось проверять все полностью с самого начала.

ЦИКЛ по символам строки до середины:
	ЦИКЛ по всем длинам, начиная с текущего символа:

В таком случае сложность составляет O(n2). Но существуют методы4, позволяющие сделать это еще быстрее.

Один из самых изящных — это алгоритм Манакера5. Он основан на методе, описанном выше, но его временная сложность сокращена до O(n).

Когда палиндромы в строке находятся далеко, оптимизировать нечего. Сложность и так O(n). Проблема появляется, когда они пересекаются, а худшим случаем является строка, состоящая из одной буквы.

Рассмотрим следующую ситуацию. Алгоритм нашел самый короткий зеленый палиндром, самый длинный голубой палиндром и остановился на букве «i»:

Числа внизу - это половина от максимального размера палиндрома с серединой в этом символе

Числа внизу — это половина от максимального размера палиндрома с серединой в этом символе

Внимательно посмотрев на картинку, можно заметить, что у нас нет необходимости обрабатывать правую часть голубого палиндрома. По определению, это зеркальное отражение левой части, так что полученную левую часть мы можем отразить на правую и получить ее, так сказать, «за бесплатно».

Случай A

Случай A

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

Случай B

Случай B

Опять-таки нет нужды дважды проверять длину отраженного палиндрома: буквы b и x обязаны быть различными, иначе голубой палиндром был бы длиннее.

Наконец, один палиндром может «касаться» другого изнутри. В этом случае нет гарантий, что отраженный палиндром не имеет бОльшую длину, так как мы получаем нижнюю границу его длины:

Случай C

Случай C

В идеале мы должны пропускать как нулевые, так и строго ненулевые значения (= все случаи, кроме последнего) в дальнейшей обработке (код 1 ниже). Но в практике (если вообще можно говорить о практике в такой абстрактной задаче) разница между ≥ и = довольно мала (всего одно дополнительное сравнение), поэтому имеет смысл рассматривать все ненулевые значения с помощью ≥ для краткости и читаемости кода (код 2 ниже).

Одна из возможных реализаций алгоритма на питоне:

#код 1
def odd(s):
    n = len(s)
    h = [0] * n
    C = R = 0      # центр и радиус или крайний правый палиндром
    besti, bestj = 0, 0     # центр и радиус самого длинного палиндрома
    for i in range(n):
        if i < C + R:         # если есть пересечение
            j = h[C-(i-C)]       # отражение
            if j < C + R - i:    # случай A
                h[i] = j
                continue
            elif j > C + R - i:  # случай B 
                h[i] = C + R - i
                continue
            else:                # case C 
                pass
        else:                 # если нет пересечения
            j = 0
        while i-j > 0 and i+j<n-1 and s[i-j-1] == s[i+j+1]:
            j += 1
        h[i] = j
        if j > bestj:
            besti, bestj = i, j
        if i + j > C + R:
            C, R = i, j
    return s[besti-bestj : besti+bestj+1]

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

Эта функция ищет палиндромы только нечетного размера. Общий подход для работы с палиндромами четного размера таков:

  • вставлять произвольный символ между символами в оригинальной строке, к примеру ‘noon’ -> ‘|n|o|o|n|’,

  • находить палиндром нечетного размера,

  • удалять произвольные символы из результата.

Символ «|» необязательно должен отсутствовать в строке. Можно использовать любой символ.

def odd_or_even(s):
    return odd('|'+'|'.join(s)+'|').replace('|', '')

>>> odd_or_even('afternoon')
'noon'

Немного запутанная версия (труднее для понимания, немного медленнее, но короче) выглядит так:

#код 2
import re

def odd(s):
    n = len(s)
    h = [0] * n
    C = R = 0      # центр и радиус или крайний правый палиндром
    besti, bestj = 0, 0     # центр и радиус самого длинного палиндрома
    for i in range(n):
        j = 0 if i > C+R else min(h[C-(i-C)], C+R-i)
        while i-j > 0 and i+j<n-1 and s[i-j-1] == s[i+j+1]:
            j += 1
        h[i] = j
        if j > bestj:
            besti, bestj = i, j
        if i + j > C + R:
            C, R = i, j
    return s[besti-bestj : besti+bestj+1]

def manacher(s):
    clean = re.sub('W', '', s.lower())
    return odd('|'+'|'.join(clean)+'|')[1::2]

>>> manacher('He said: "Madam, I'm Adam!"')
'madamimadam'

Как видно, в коде есть два вложенных цикла. Тем не менее, интуитивно понятно, почему сложность O(n). На диаграмме показан массив h.

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

Очевидно из диаграммы, что, когда палиндромы не пересекаются, число шагов «вверх» равно количеству горизонтальных «пропускающих» шагов. Для пересекающихся палиндромов чуть более заморочено, но если посчитать число шагов «вверх» и число горизонтальных «пропускающих шагов, то эти числа вновь совпадут. Так что общее число шагов ограничено 2n сравнениями. Не просто n , потому что, в отличие от вертикальных шагов, чтобы понять, пропускать ли горизонтальный шаг или нет, необходимо проделать некую работу (хотя можно изменить реализацию, чтобы пропускать их за постоянное время). Итого временная сложность — O(n).

Алгоритм Манакера позволяет найти самый длинный палиндром в строке (если быть точнее, не просто самый длинный палиндром, а самый длинный палиндром для всех возможных центров) за линейное время, используя очень интуитивный подход, лучше всего описываемый визуально.


Ссылки:

  1. Сайт Big-O Cheat Sheet.

  2. Статья на Википедии про палиндромные последовательности.

  3. Задача на Leetcode про самый длинный палиндром в строке.

  4. Статья на Википедии про самый длинный палиндром в строке.

  5. Гленн Манакер (1975), «Новый линейный алгоритм для поиска самого длинного палиндрома строки», журнал ACM.

Классическая задача. Найдите в данной вам строке максимальную по длине подстроку, которая является палиндромом (то есть читается слева направо и справа налево одинаково). Предложите как можно более эффективный алгоритм.

Решение за О(n²) и О(1) памяти: перебор

Очевидное квадратичное решение приходит в голову практически сразу. У каждого палиндрома есть центр: символ (или пустое место между двумя соседними символами в случае палиндрома четной длины), строка от которого читается влево и вправо одинаково. Например, для палиндрома abacaba таким центром является буква c, а для палиндрома colloc — пространство между двумя буквами l. Очевидно, что центром нашей искомой длиннейшей палиндромной подстроки является один из символов строки (или пространство между двумя соседними символами), в которой мы производим поиск.

Теперь мы можем реализовать такое решение: давайте переберем все символы строки, для каждого предполагая, что он является центром искомой самой длинной палиндромной подстроки. То есть предположим, что на данный момент мы стоим в i-ом символе строки. Теперь заведем две переменных left и right, изначально left = i - 1 и right = i + 1 для палиндромов нечетной длины и i - 1, i соответственно для палиндромов четной длины. Теперь будем проверять, равны ли символы в позициях строки left и right. Если это так, то уменьшим left на 1, а right увеличим на 1. Будем продолжать этот процесс до тех пор, пока символы в соответствующих позициях станут не равны, или же мы не выйдем за границы массива. Это будет означать, что мы нашли самый длинный палиндром в центре с i-ым символов в случае для палиндрома нечетной длины и в пространстве между i-ым и i - 1-ым символом в случае палиндрома четной длины. Выполним такой алгоритм для всех символов строки, попутно запоминая найденный максимум, и таким образом мы найдем самую длинную палиндромную подстроку всей строки.

Докажем, что это решение работает за O(n²). Рассмотрим строку ааааааааааааааа… Для каждого ее символа мы будем двигать left и right, пока не выйдем за границы массива. То есть для первого символа мы сделаем 0*2 (умножение на 2 происходит, потому что мы выполняем алгоритм два раза — для палиндромов нечетной и четной длины) итераций, для второго 1*2, для третьей 2*2, и т.д. до центра, потом кол-во итераций станет уменьшаться. Это арифметическая прогрессия с разностью 2. Рассмотрим сумму этой арифметической прогрессии до середины строки. Как известно, сумма арифметической прогрессии имеет формулу (A1+An)/2*n. В нашем случае A1 = 0, An = n/2*2 = n. (0+n)/2*n = n/2*n = O(n²). Для убывающей части все аналогично, там тоже получится O(n²). O(n²)+O(n²) = O(n²), ч.т.д.

Решение за О(n log n) по времени и О(n) памяти: полиномиальный хэш + бинпоиск

Это решение является ускоренной модификацией предыдущего. Можно посчитать для строки полиномиальный хеш, замечательным свойством которого является то, что мы можем за О(1) получить хеш любой подстроки, а значит, посчитав его для оригинальной и перевернутой строки мы можем за О(1) проверить, является подстрока [l..r] палиндромом (реализацию можно найти здесь). Следующее замечание состоит в том, что для каждого центра при переборе подстрока на некотором количестве итераций сначала будет являться палиндромом, а затем всегда нет. А это значит, что мы можем воспользоваться бинпоиском: переберем все символы, для каждого бинпоиском найдем максимальную палиндромную подстроку с центром в нем, по ходу дела будем запоминать найденный максимум.

Очевидно, что это решение работает за О(n log n) по времени. Мы перебираем все n символов, для каждого совершаем O(log n) итераций бинпоиска, на каждой из который проверяем, является ли подстрока палиндромом. В итоге: O(n log n) по времени и О(n) по памяти (потому что нам наобходимо хранить посчитанные хеши).

Решение за О(n) времени и O(n) памяти: алгоритм Манакера

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

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

Написал такой вот код, но он не проходит все тесты:

n = int(input())
str = input()
l = list()
for i in range(len(str)):
    l.insert(i, ord(str[i]))
l = sorted(l)
for i in range(len(l)):
    l[i] = chr(l[i])


    # каждая буква встречается четное количество раз
even = False 
one_even = False
e = l[0]
count = 0
for i in range(len(l)):
    if l[i] == e:
        count += 1
    elif count % 2 == 0:
        even = True
        one_even = True
        e = l[i]
        count = 1
    else:
        even = False
        break
    if count % 2 != 0 and i == (len(l) - 1):
        even = False
        
e = l[0]
count = 0
res_str = ""
if even:
    for i in range(len(l)):
        if l[i] == e:
            count += 1 
        else:
            res_str += e * int((count / 2))
            count = 1
            e = l[i]
        if i == (len(l) - 1):
            res_str += e * int((count / 2))
    res_str2 = ""
    for i in reversed(res_str):
        res_str2 += i
    print(res_str + res_str2)

    # ни одна буква не встречается четное количество раз
elif not one_even:
    print(l[0])

    # некоторые буквы встречаются нечетное количество раз
else:
    s = set(l)
    s = list(s)
    res_str = ""
    odd = ""
    count = 0
    fiend_Odd = False
    for i in range(len(s)):
        for j in range(len(l)):
            if s[i] == l[j]:
                count += 1
            if j == (len(l) - 1):
                if count % 2 == 0:
                    res_str += s[i] * int(count / 2)
                    count = 0
                elif not fiend_Odd:
                    fiend_Odd = True
                    odd += s[i] * int(count)
                    count = 0
    res_str2 = ""
    for i in reversed(res_str):
        res_str2 += i
    print(res_str + odd + res_str2)

entithat's user avatar

entithat

13.1k2 золотых знака19 серебряных знаков37 бронзовых знаков

задан 9 апр 2021 в 17:46

Валерий Тарасов's user avatar

  • Подсчитываем количество каждой из букв в строке
  • Для букв, количество которых больше 2, добавляем половину в результат
  • Вычитаем из счётчиков букв уже использованное количество
  • Добавляем в результирующую строку любую оставшуюся букву со счётчиком 1 (если таких нет, то добавляем пусто)
  • и дописываем в конец реверснутую результирующую строку (которая на самом деле была половина палиндрома)
from collections import Counter

letters = "ABCDABCF"
letters_num = Counter(letters)
result = ""
for letter in letters_num:
    result += letter * (letters_num[letter] // 2)
    letters_num[letter] -= letters_num[letter] // 2 * 2
result += dict(map(reversed, letters_num.items())).get(1,'') + result[::-1]
print(result)

ответ дан 9 апр 2021 в 18:35

GrAnd's user avatar

GrAndGrAnd

13.4k1 золотой знак8 серебряных знаков23 бронзовых знака

Решил не исправлять ваше решение, а написать своё, если вдруг пригодится. Код выводит ответ в сортированном виде.

Если важно было исправить ваше, то просто мой ответ игнорируйте.

Пробовать код онлайн!

n, s = int(input()), sorted(input())
assert len(s) == n, (n, len(s))
cnt = {}
for c in s:
    if c not in cnt:
        cnt[c] = 0
    cnt[c] += 1
odd = None
for k, v in cnt.items():
    if v % 2:
        if odd is None:
            odd = (k, v)
        else:
            cnt[k] -= 1
if odd is not None:
    del cnt[odd[0]]
cnt = dict(sorted(cnt.items()))
p = (''.join(k * (v // 2) for k, v in cnt.items()) +
    (odd[0] * ((odd[1] + 1) // 2) if odd is not None else ''))
p += ''.join(reversed(p[:len(p) - (1, 0)[odd is None]]))
print(p)

Ввод:

8
abcdefab

Вывод:

abcba

Ввод:

4
abab

Вывод:

abba

ответ дан 9 апр 2021 в 18:32

Arty's user avatar

ArtyArty

3,0027 серебряных знаков19 бронзовых знаков

Если коротко:

  • Считаем кол-во букв в даной строке. У нас получится примерно такой массив: [ 'a1', 'b1', 'c0' ].

  • Для каждой строки, кол-во повторений какой больше 1 добавляем в результат.

  • И в конце просто дублируем полученную строку и прибавляем ее в конец.


s = 'abbca'
a = sorted(list(set(map(lambda x: f'{x}{s.count(x)}', s))), key=lambda x: -int(x[1:]))
for item in a:
    letter, number = item[0], int(item[1:]) // 2
    if number >= 1:
        o += letter * number
    else:
        o += letter
        break

o += o[:-1][::-1] if int(a[-1][1:]) // 2 == 0 else o[::-1]  
print(o) # abcba

P.S. Правда если в палиндром будет состоять из цифр, то надо поправить чуть-чуть.

ответ дан 9 апр 2021 в 18:19

entithat's user avatar

entithatentithat

13.1k2 золотых знака19 серебряных знаков37 бронзовых знаков

def palindrome(letters):
    counter = {}
    single = ''
    result = ''

    for ch in letters:
        if ch not in counter:
            counter[ch] = 1
        else:
            counter[ch] += 1
            if counter[ch] == 2:
                counter[ch] = 0
                result += ch

    for ch, count in counter.items():
        if count == 1:
            single = ch
            break

    return result + single + result[::-1]


print(palindrome('aab'))    # aba
print(palindrome('qazqaz')) # qazzaq
print(palindrome('abcdef')) # a

ответ дан 9 апр 2021 в 19:00

slippyk's user avatar

slippykslippyk

6,0913 золотых знака19 серебряных знаков38 бронзовых знаков

Improve Article

Save Article

Like Article

  • Read
  • Discuss
  • Improve Article

    Save Article

    Like Article

    Given a string, count how many maximum-length palindromes are present. (It need not be a substring)

    Examples: 

    Input : str = "ababa"
    Output: 2
    Explanation : 
    palindromes of maximum of lengths are  :
     "ababa", "baaab" 
    
    Input : str = "ababab"
    Output: 4
    Explanation : 
    palindromes of maximum of lengths are  :
     "ababa", "baaab", "abbba", "babab"

    Approach A palindrome can be represented as “str + t + reverse(str)”

    Note: “t” is empty for even length palindromic strings 

    Calculate in how many ways “str” can be made and then multiply with “t” (number of single characters left out).
    Let ci be the number of occurrences of a character in the string. Consider the following cases: 

    1. If ci is even. Then half of every maximum palindrome will contain exactly letters fi = ci / 2. 
    2. If ci is odd. Then half of every maximum palindrome will contain exactly letters fi = (ci – 1)/ 2.

    Let k be the number of odd ci. If k=0, the length of the maximum palindrome will be even; otherwise it will be odd and there will be exactly k possible middle letters i.e., we can set this letter to the middle of the palindrome.
    The number of permutations of n objects with n1 identical objects of type 1, n2 identical objects of type 2, and n3 identical objects of type 3 is n! / (n1! * n2! * n3!)
    So here we have total number of characters as fa+fb+fa+…….+fy+fz . So number of permutation is (fa+fb+fa+…….+fy+fz)! / fa! fb!…fy!fz!.
    Now If K is not 0, it’s obvious that the answer is k * (fa+fb+fa+…….+fy+fz+)! / fa! fb!…fy!fz!

    Below is the implementation of the above.

    C++

    #include <bits/stdc++.h>

    using namespace std;

    int fact(int n)

    {

        int ans = 1;

        for (int i = 1; i <= n; i++)   

            ans = ans * i;

        return (ans);

    }

    int numberOfPossiblePalindrome(string str, int n)

    {  

        unordered_map<char, int> mp;

        for (int i = 0; i < n; i++)

            mp[str[i]]++;

        int k = 0; 

        int num = 0; 

        int den = 1; 

        int fi; 

        for (auto it = mp.begin(); it != mp.end(); ++it)

        {

            if (it->second % 2 == 0)

                fi = it->second / 2;

            else

            {

                fi = (it->second - 1) / 2;

                k++;

            }

            num = num + fi;

            den = den * fact(fi);

        }

        if (num != 0)

            num = fact(num);

        int ans = num / den;

        if (k != 0)

        {

            ans = ans * k;

        }

        return (ans);

    }

    int main()

    {

        char str[] = "ababab";

        int n = strlen(str);

        cout << numberOfPossiblePalindrome(str, n);

        return 0;

    }

    Java

    import java.util.*;

    class GFG

    {

    static int fact(int n)

    {

        int ans = 1;

        for (int i = 1; i <= n; i++)

            ans = ans * i;

        return (ans);

    }

    static int numberOfPossiblePalindrome(String str, int n)

    {

        Map<Character,Integer> mp = new HashMap<>();

        for (int i = 0; i < n; i++)

            mp.put( str.charAt(i),mp.get( str.charAt(i))==null?

                    1:mp.get( str.charAt(i))+1);

        int k = 0;

        int num = 0;

        int den = 1;

        int fi;

        for (Map.Entry<Character,Integer> it : mp.entrySet())

        {

            if (it.getValue() % 2 == 0)

                fi = it.getValue() / 2;

            else

            {

                fi = (it.getValue() - 1) / 2;

                k++;

            }

            num = num + fi;

            den = den * fact(fi);

        }

        if (num != 0)

            num = fact(num);

        int ans = num / den;

        if (k != 0)

        {

            ans = ans * k;

        }

        return (ans);

    }

    public static void main(String[] args)

    {

        String str = "ababab";

        int n = str.length();

        System.out.println(numberOfPossiblePalindrome(str, n));

    }

    }

    Python3

    import math as mt

    def fact(n):

        ans = 1

        for i in range(1, n + 1):

            ans = ans * i

        return (ans)

    def numberOfPossiblePalindrome(string, n):

        mp = dict()

        for i in range(n):

            if string[i] in mp.keys():

                mp[string[i]] += 1

            else:

                mp[string[i]] = 1

        k = 0

        num = 0

        den = 1

        fi = 0

        for it in mp:

            if (mp[it] % 2 == 0):

                fi = mp[it] // 2

            else:

                fi = (mp[it] - 1) // 2

                k += 1

            num = num + fi

            den = den * fact(fi)

        if (num != 0):

            num = fact(num)

        ans = num //den

        if (k != 0):

            ans = ans * k

        return (ans)

    string = "ababab"

    n = len(string)

    print(numberOfPossiblePalindrome(string, n))

    C#

    using System;

    using System.Collections.Generic;

    class GFG

    {

    static int fact(int n)

    {

        int ans = 1;

        for (int i = 1; i <= n; i++)

            ans = ans * i;

        return (ans);

    }

    static int numberOfPossiblePalindrome(String str, int n)

    {

        Dictionary<char,int> mp = new Dictionary<char,int>();

        for (int i = 0 ; i < n; i++)

        {

            if(mp.ContainsKey(str[i]))

            {

                var val = mp[str[i]];

                mp.Remove(str[i]);

                mp.Add(str[i], val + 1);

            }

            else

            {

                mp.Add(str[i], 1);

            }

        }

        int k = 0;

        int num = 0;

        int den = 1;

        int fi;

        foreach(KeyValuePair<char, int> it in mp)

        {

            if (it.Value % 2 == 0)

                fi = it.Value / 2;

            else

            {

                fi = (it.Value - 1) / 2;

                k++;

            }

            num = num + fi;

            den = den * fact(fi);

        }

        if (num != 0)

            num = fact(num);

        int ans = num / den;

        if (k != 0)

        {

            ans = ans * k;

        }

        return (ans);

    }

    public static void Main(String[] args)

    {

        String str = "ababab";

        int n = str.Length;

        Console.WriteLine(numberOfPossiblePalindrome(str, n));

    }

    }

    Javascript

    <script>

        function fact(n)

        {

            let ans = 1;

        for (let i = 1; i <= n; i++)

            ans = ans * i;

        return (ans);

        }

    function numberOfPossiblePalindrome(str,n)

    {

        let mp = new Map();

        for (let i = 0; i < n; i++)

            mp.set( str[i],mp.get( str[i])==null?

                    1:mp.get( str[i])+1);

        let k = 0;

        let num = 0;

        let den = 1;

        let fi;

        for (let [key, value] of mp.entries())

        {

            if (value % 2 == 0)

                fi = value / 2;

            else

            {

                fi = (value - 1) / 2;

                k++;

            }

            num = num + fi;

            den = den * fact(fi);

        }

        if (num != 0)

            num = fact(num);

        let ans = Math.floor(num / den);

        if (k != 0)

        {

            ans = ans * k;

        }

        return (ans);

    }

    let str = "ababab";

    let n = str.length;

    document.write(numberOfPossiblePalindrome(str, n));

    </script>

    Time Complexity: O(n), where n is the length of the given string.
    Auxiliary Space: O(n)

    Last Updated :
    24 Nov, 2022

    Like Article

    Save Article

    Определение:
    Палиндромом (англ. Palindrome) называется строка, которая одинаково читается как слева направо, так и справа налево.
    Определение:
    Подпоследовательностью-палиндромом данной строки (англ. Largest Palindromic Subsequence) называется последовательность символов из данной строки, не обязательно идущих подряд, являющаяся палиндромом.

    Например, HELOLEH является подпоследовательностью-палиндромом строки HTEOLFEOLEH.

    Задача:
    Задача о наибольшей подпоследовательности-палиндроме — это задача о поиске наибольшей подпоследовательности, которую можно получить вычеркиванием некоторых букв из данной последовательности таким образом, что оставшаяся подпоследовательность будет палиндромом.

    Содержание

    • 1 Решение
      • 1.1 Асимптотика
    • 2 Пример
    • 3 Псевдокод
    • 4 См. также
    • 5 Источники информации

    Решение

    Обозначим данную последовательность через , а ее элементы — через , где — длина строки . Будем рассматривать возможные подпоследовательности данной последовательности с го по ый символ включительно, обозначив её как . Длины максимальных подпалиндромов для данной последовательности будем записывать в двумерный массив : — длина максимальной подпоследовательности-палиндрома, который можно получить из последовательности .

    Начнем решать задачу с простых подпоследовательностей. Для последовательности из одного элемента (то есть подпоследовательности вида ) ответ очевиден — ничего вычеркивать не надо, такая строка будет искомой подпоследовательностью-палиндромом. Для последовательности из двух элементов возможны два варианта: если элементы равны, то мы имеем подпоследовательность-палиндром, ничего вычеркивать не надо. Если же элементы не равны, то вычеркиваем любой.

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

    Асимптотика

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

    Пример

    Рассмотрим решение на примере последовательности ABACCBA. Первым делом заполняем диагональ массива единицами, они будут соответствовать подпоследовательностями из одного элемента. Затем начинаем рассматривать подпоследовательности длины два. Во всех подпоследовательностях, кроме , элементы различны, поэтому в соответствующие ячейки запишем , а в — .

    Получается, что мы будем заполнять массив по диагоналям, начиная с главной диагонали. Для подпоследовательностей длины получаются следующие значения: в подпоследовательности ABA первый и последний элемент равны, поэтому . В остальных подпоследовательностях первый и последний элементы различны.

    BAC:

    ACC:

    CCB:

    CBA:

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

    Если же в задаче необходимо вывести не длину, а саму подпоследовательность-палиндром, то дополнительно к массиву длин мы должны построить массив переходов — для каждой ячейки запомнить, какой из случаев был реализован.

    Последовательность заполнения массива и массив переходов см. на изображениях ниже.

    Заполнение массива длин (1)
    Заполнение массива длин (2)
    Заполнение массива длин (3)
    Заполнение массива длин (4)
    Массив переходов

    Псевдокод

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

    Функция для вычисления длины палиндрома:

    • Границы исходной последовательности:

     int palSubSeq(left: int, right: int):
       if L[left][right] == -1 
         if s[left] == s[right] 
           L[left][right] = palSubSeq(left + 1, right - 1) + 2
         else 
           L[left][right] = max(palSubSeq(left + 1, right), palSubSeq(left, right - 1))  
       return L[left][right]
    


    Процедура для построения искомого палиндрома:

    • Границы исходной последовательности:
    • Границы искомой подпоследовательности-палиндрома, где правой границей будет длина найденного палиндрома:

     // palindrome — массив символов, где в palindrome[i] содержится символ искомой последовательности-палиндрома
     palChars(left: int, right: int, palLeft: int, palRight: int):
       while left  right
         if left == right and L[left][right] == 1
           palindrome[palLeft++] = S[left++]
         else
           if S[left] == S[right]
             palindrome[palLeft++] = S[left++]
             palindrome[palRight--] = S[right--]
           else
             if L[left + 1][right]  L[left][right - 1]
               left++
             else
               right--
    

    См. также

    • Задача о наибольшей общей подпоследовательности
    • Задача о наибольшей возрастающей подпоследовательности
    • Задача о наибольшей общей палиндромной подпоследовательности

    Источники информации

    • Википедия — Палиндром
    • Wikipedia — Palindrome
    • Wikipedia — Longest palindromic subsequence

    Понравилась статья? Поделить с друзьями:
  • Как найди видео по gif
  • Фар край 5 судья пума как найти
  • Как найти модель на съемку бесплатно
  • Как найти забыть эту сеть
  • Как найти аналог для микросхемы