Что такое палиндромы как найти

Алгоритмы для поиска палиндромов

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

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

image

Сегодня я хочу вам рассказать об алгоритмах подсчёта количества палиндромов в строке: для чего это нужно, где применяется, как это быстро сделать, какие подводные камни нас ожидают и многое другое. Рассмотрим различные способы для решения данной задачи, выясним плюсы и минусы каждого способа. Эта статья будет обзорной: если я что-то не описываю здесь, то постараюсь всегда дать вам набор ссылок, где всё подробно описано и расписано. Надеюсь, что материал будет интересен как новичкам в сфере алгоритмов, так и матёрым программистам. Что же, если я смог заинтересовать вас, то прошу под кат!

Для начала стоит вспомнить, что же такое палиндром.

Палиндро́м — число (например, 404), буквосочетание, слово или текст, одинаково читающееся в обоих направлениях. Иногда палиндромом называют любой симметричный относительно своей середины набор символов.(выдержка из Википедии)

Определение очень лёгкое и понятное. Мы в данном посте будем рассматривать палиндромы-слова(поп, кок и т.д.). Но это не значит, что алгоритмы неприменимы для иных ситуаций. Просто чуть сузим область.

Для чего может потребоваться искать палиндромы?

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

Одно из основных применений — спортивное/олимпиадное программирование. Там задач на такое хватает. Задач понаписывают, а участникам уже думай, каким способом это решить. Из личного опыта скажу, что мне такие задачи встречались всего пару раз, но все они были из разряда «вот тебе задача, ты не думай, а просто напиши алгоритм». То есть не интересные задачи, которые развивают просто механическую память по набиванию алгоритмов.

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

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

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

0) Самый банальный алгоритм с асимптотикой O(N^3)

bool SlowN3::isPalindrom(int leftBorder, int rightBorder)
{
    while(leftBorder <= rightBorder)
    {
        if(str[leftBorder] != str[rightBorder])
        {
            return false;
        }
        ++leftBorder;
        --rightBorder;
    }
    return true;
}

long long SlowN3::operator ()()
{
    for(int leftBorder = 0;leftBorder < str.length(); ++leftBorder)
    {
        for(int rightBorder = leftBorder + 1; rightBorder < str.length(); ++rightBorder)
        {
            if( isPalindrom(leftBorder, rightBorder) )
            {
                ++cntPalindromes;
            }
        }
    }
    return cntPalindromes;
}

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

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

Но это всё ужасно медленно работает. Почему? Да потому что мы квадрат от N раз (N у нас будет всегда длина строки) перебираем границы текущей подстроки, а потом ещё и за O(N) в худшем случае выполняем проверку для каждой подстроки на палиндромность.

Плюсов у данного способа немного:

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

Минусы:

— Крайне малая скорость работы. Как вы можете видеть, что при строке из тысячи букв ‘a’ данный алгоритм сделает порядка 10^9 итераций, что есть очень плохо. А что если строка длиной 100000?

1) Самый банальный алгоритм с асимптотикой O(N^2)

Код:

void SlowN2::oddCount()
{
    for(int indMiddle = 0; indMiddle < str.length(); ++indMiddle)
    {
        int leftBorder = indMiddle - 1, rightBorder = indMiddle + 1;
        while(leftBorder >= 0 && rightBorder < str.length() && str[leftBorder] == str[rightBorder])
        {
            ++cntPalindromes;
            --leftBorder;
            ++rightBorder;
        }
    }
}

void SlowN2::evenCount()
{
    for(int indMiddle = 0; indMiddle < str.length(); ++indMiddle)
    {
        int leftBorder = indMiddle, rightBorder = indMiddle + 1;
        while(leftBorder >= 0 && rightBorder < str.length() && str[leftBorder] == str[rightBorder])
        {
            ++cntPalindromes;
            --leftBorder;
            ++rightBorder;
        }
    }
}

Здесь уже чуточку интереснее. У нас имеется строка, и два временных массива для палиндромов чётной и нечётной длины. А число в ячейке i массива будет хранить количество палиндромов в строке s(исходная строка) с центром в точке i. Для нечётной длины палиндрома центр находится легко, ну а для чётной мы просто чуть поиграемся с индексами и сместим чуточку центр(что видно в коде). Наш результат это есть сумма чисел из двух массивов.

Как видите, снова ничего сложного. Но работает это заметно быстрее предыдущего алгоритма. Почему? Давайте рассмотрим, как же оно всё работает.

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

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

Рассмотрим плюсы и минусы способа.

Плюсы:

+ Всё также легко кодится. Ошибиться очень сложно.
+ Малая скрытая константа
+ Хорошо ведёт себя на случайных строках

Минусы:

— Всё ещё долгое время работы

После этого способа уже голова начинает думать-думать-думать. И тут идея! А что если мы модифицируем этот способ, только будем от центрального элемента прыгать не на 1 символ с каждой итерацией, а на чуточку больше? И тут нам на помощь приходят…

2) Используем хеши!

Этот способ чуточку сложнее, поэтому сразу приведу код, а потом постараюсь обьяснить, что за магия там творится(хотя магии нет, как вы сами прекрасно знаете):

inline long long Hash::getHash(int indLeft, int indRight) const
{
    return prefixHash[indRight] - (indLeft > 0 ? prefixHash[indLeft - 1] : 0);
}

inline long long Hash::getRevHash(int indLeft, int indRight) const
{
    return suffixHash[indLeft] - (indRight < suffixHash.size() - 1 ? suffixHash[indRight + 1] : 0);
}

void Hash::PrepareSimpleNumbers()
{
    if(str.length() > simpleNumbers.size())
    {
        size_t oldSize = simpleNumbers.size();
        simpleNumbers.resize(str.length());
        simpleNumbers[0] = 1LL;
        for(int i = oldSize; i < simpleNumbers.size(); ++i)
            simpleNumbers[i] = simpleNumbers[i - 1] * SimpleBase;
    }
}

void Hash::CountingPrefixHash()
{
    prefixHash[0] = str[0];
    for(int i = 1; i < prefixHash.size(); ++i)
    {
        prefixHash[i] = prefixHash[i - 1] + str[i] * simpleNumbers[i];
    }
}

void Hash::CountingSuffixHash()
{
    suffixHash[suffixHash.size() - 1] = str[str.length() - 1];
    for(int i = (int) str.length() - 2, j = 1; i >= 0; --i, ++j)
    {
        suffixHash[i] = suffixHash[i + 1] + str[i] * simpleNumbers[j];
    }
}

bool Hash::isPalindrome(int indLeft, int indRight) const
{
    return getHash(indLeft, indRight) * simpleNumbers[prefixHash.size() - indRight - 1] ==
           getRevHash(indLeft, indRight) * simpleNumbers[indLeft];
}

void Hash::CountingOdd()
{
    for (int i = 0; i < oddCount.size(); i++)
    {
        int indLeft = 1, indRight = min(i + 1, static_cast<int> (oddCount.size() - i));
        while (indLeft <= indRight)
        {
            int middle = (indLeft + indRight) / 2;
            if (isPalindrome(i - middle + 1, i + middle - 1))
            {
                oddCount[i] = middle - 1;
                indLeft = middle + 1;
            }
            else
            {
                indRight = middle - 1;
            }
        }
    }
}

void Hash::CountingEven()
{
    for (int i = 0; i < evenCount.size(); i++)
    {
        int indLeft = 1, indRight = min(i, static_cast<int> (evenCount.size() - i));
        while (indLeft <= indRight)
        {
            int middle = (indLeft + indRight) / 2;
            if (isPalindrome(i - middle, i + middle - 1))
            {
                evenCount[i] = middle;
                indLeft = middle + 1;
            }
            else
            {
                indRight = middle - 1;
            }
        }
    }
}

long long Hash::operator()()
{
    PrepareSimpleNumbers();
    CountingPrefixHash();
    CountingSuffixHash();
    CountingOdd();
    CountingEven();
    for(int i = 0; i < str.length(); ++i)
    {
        cntPalindromes += oddCount[i] + evenCount[i];
    }
    return cntPalindromes;
}

Краткая суть данного способа: мы перебираем центральный элемент нашего палиндрома, и потом дихотомией стараемся найти наибольший радиус нашего палиндрома (под радиусом здесь понимается расстояние от центрального элемента до крайнего, если у нас палиндром чётной длины, то просто добавим чуточку игры с индексами, чтобы всё работало). Во время подбора мы должны как-то быстро сравнивать подстроки на идентичность. делаем это с помощью хешей. Асимптотика, как легко догадаться: N тратим на перебор центрального элемента, logN в худшем случае тратим на подбор радиуса палиндрома, за единицу сравниваем подстроки с помощью хешей. Итого имеем O(NlogN), что очень даже неплохо на самом деле.

Теперь чуть подробнее остановимся на данном методе.

На чём основывается наш метод? Так как мы перебираем центральный элемент, а потом дихотомией подбираем радиус наибольшего палиндрома с центром в данном элементе, то мы должны быстро брать хеш левой части нашего потенциального палиндрома и сравнивать его с хешем правой части нашего потенциального палиндрома.

Как это сделать? Давайте предварительно рассчитаем хеш для каждого префикса и суффикса исходной строки. В коде это у нас делают методы CountingPrefixHash() и CountingSuffixHash() соответственно.

С помощью методов getHash() и getRevHash() мы можем быстро получить хеши левой и правой частей рассматриваемого на данном этапе потенциального палиндрома. Тут может возникнуть вопрос, почему нельзя воспользоваться одной и той же функцией подсчёта хеша для левой и правой частей. А всё потому, что мы при проверке на палиндромность левую часть мы читаем слева направо, а вторую справа налево. И нам необходимо поддерживать хеш слева направо и справа налево.

Осталась единственная сложность: как сравнить этих 2 хеша? И эту проблему мы решаем с помощью метода isPalindrome(), который приводит хеши к одному основанию и сравнивает их.

Результатом каждой итерации у нас будет количество палиндромов с центром i. Пробегаемся 2 раза для палиндромов чётной и нечётной длины, ответ на нашу задачу есть сумма всех значений массивов oddCount и evenCount

Более подробно про данный метод вы можете почитать в этой статье.

Плюсы:

+ Асимптотически мы снизили до NlogN, что не может не радовать. Если брать только худшие случаи, то в теории когда-нибудь мы выиграем

Минусы:

— Довольно тяжело пишется. Легко посеять багу. Нужна немного теоретическая подготовка в области хешей.
— Медленно работает на случайных тестах. Вы можете сами в этом убедиться. А всё из-за большой скрытой константы и из-за того, что даже если у нас нет ни одного палиндрома с центром в данном элементе, то алгоритм сделает logN прыжков, а это всё занимает время.
— Коллизии. Как вы видите, в моём исходнике используется хеш, который обычно пишут на олимпиадах по программированию(да-да, я немного этим когда-то занимался). Так вот, на соревнованиях данный способ показывает себя хорошо. Лично у меня коллизии не случались. Но я знаю про способы спокойно данный хеш завалить(в частности способ с использованием строк Туэ-Морса). Я когда-то слышал, что есть какой-то фиббоначиевый хеш, который вроде бы не ломается на данном тесте, но информация недостоверная. Так что будьте осторожны с коллизиями.

А если мы хотим 100% решение без всякой там коллизийной поправки и ввода хеша по другому основанию и так далее?

3) Алгоритм Манакера

Код:

//Find palindroms like 2*N+1
void Manacker::PalN2()
{
    int leftBorder = 0, rightBorder = -1, tempMirror;//start digits for algortihm
    for(int i = 0; i < str.length(); ++i)
    {
        tempMirror = (i > rightBorder ? 0 : std::min(ansPalN2[leftBorder + rightBorder - i], 
                                                                            rightBorder - i)) + 1;//find mirror of current index
        while(i + tempMirror < str.length() && i - tempMirror >= 0 && 
                 str[i - tempMirror] == str[i + tempMirror])//increase our index
        {
            ++tempMirror;
        }
        ansPalN2[i] = --tempMirror;
        if(i + tempMirror > rightBorder)//try to increase our right border of palindrom
        {
            leftBorder = i - tempMirror;
            rightBorder = i + tempMirror;
        }
    }
}

void Manacker::Pal2()
{
    int leftBorder = 0, rightBorder = -1, tempMirror;
    for(int i = 0; i < str.length(); ++i)
    {
        tempMirror = (i > rightBorder ? 0 : std::min(ansPal2[leftBorder + rightBorder - i + 1],
                                                                            rightBorder - i + 1)) + 1;
        while(i + tempMirror - 1 < str.length() && 
                  i - tempMirror >= 0 && str[i - tempMirror] == str[i + tempMirror - 1])
        {
            ++tempMirror;
        }
        ansPal2[i] = --tempMirror;
        if(i + tempMirror - 1 > rightBorder)
        {
            leftBorder = i - tempMirror;
            rightBorder = i + tempMirror - 1;
        }
    }
}

Итак, мы получили с помощью хешей алгоритм за NlogN. Но хочется быстрее. Хочется чего-то линейного. И тут нам на помощь спешит Алгоритм Манакера (по ссылке вы также можете видеть реализацию алгоритма на Java), который мало того, что линеен, так ещё и обладает крайне малой скрытой константой, что положительно сказывается на его скорости работы. Подробно описывать способ я не буду, так как это получится пересказ этой замечательной ссылки (я сам учил алгоритм именно по этой ссылке). Здесь я приведу слегка пересказанное объяснение.

Алгоритм заключается в том, что мы обрабатываем строку символ за символом, поддерживая самый правый палиндром в нашей строке. И на каждой итерации смотрим, наш текущий элемент находится внутри границ самого правого палиндрома или нет. Если находится, то мы можем извлечь ответ из ранее посчитанных значений, путём нехитрых манипуляций с индексами. Если же не находится, то мы идём точно таким же алгоритмом, который описан в пункте 1) этого обзора: идём символ за символом и сравниваем зеркальные элементы относительно центра. Идём до тех пор, пока они равны. И не забываем обновить после этого границы самого правого найденного палиндрома. Это вкратце. Про некоторые подводные камни вы можете почитать в приведённой статье.

Что ещё интересного: во всех задачах, которые я решал на контестах (по олимпиадному программированию), хватало именно этого алгоритма. Очень просто пишется, если ты его писал N раз дома и уже знаешь наизусть.

Плюсы:

+ Довольно короткий код.
+ Очень быстро работает. Мало того, что асимптотика O(N), так ещё и малая скрытая константа.

Минусы:

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

А есть ли другие способы решить данную задачу за линейное время?

4) Дерево палиндромов

Немного предыстории. Эту относительно новую структуру данных открыл Михаил Рубинчик rumi13 и представил её на Петрозаводских сборах. Структура крайне интересная своей с одной стороны простотой, а с другой тем, что позволяет быстро решать довольно широкий спектр задачи про палиндромы. И самое главное — позволяет довольно просто считать количество палиндромов в строке за O(N) (но само дерево палиндромов думаю придумывалось далеко не для этого, а для более серьёзных задач с палиндромами).

Код:

PalindromicTree::PalindromicTree(const std::string& s) : FindPalindrome(s)
{
    initTree();
}

PalindromicTree::~PalindromicTree()
{
    for(int i = 0;i < pullWorkNodes.size(); ++i)
    {
        delete pullWorkNodes[i];
    }
}

void PalindromicTree::initTree()
{
    root1 = new Node;
    root1->len = -1;
    root1->sufflink = root1;
    root2 = new Node;
    root2->len = 0;
    root2->sufflink = root1;
    suff = root2;
    pullWorkNodes.push_back(root1);
    pullWorkNodes.push_back(root2);
}

void PalindromicTree::findAddSuffix(Node* &cur, int pos, int& curlen)
{
    while (true)
    {
        curlen = cur->len;
        if (pos - 1 - curlen >= 0 && str[pos - 1 - curlen] == str[pos])
        {
            break;
        }
        cur = cur->sufflink;
    }
}

void PalindromicTree::makeSuffixLink(Node* &cur, int pos, int& curlen,int let)
{
    while (true)
    {
        cur = cur->sufflink;
        curlen = cur->len;
        if (pos - 1 - curlen >= 0 && str[pos - 1 - curlen] == str[pos])
        {
            suff->sufflink = cur->next[let];
            break;
        }
    }
}

void PalindromicTree::addLetter(int pos)
{
    Node* cur = suff;
    int let = str[pos] - 'a', curlen = 0;
    findAddSuffix(cur, pos, curlen);
    if (cur->next[let] != nullptr)
    {
        suff = cur->next[let];
        return;
    }
    suff = new Node;
    pullWorkNodes.push_back(suff);
    suff->len = cur->len + 2;
    cur->next[let] = suff;
    if (suff->len == 1)
    {
        suff->sufflink = root2;
        suff->num = 1;
        return;
    }
    makeSuffixLink(cur, pos, curlen, let);
    suff->num = 1 + suff->sufflink->num;
}

long long PalindromicTree::operator ()()
{
    for (int i = 0; i < str.length(); ++i)
    {
        addLetter(i);
        cntPalindromes += suff->num - 1;
    }
    return cntPalindromes;
}

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

Итак, идея дерева. В вершинах нашего дерева будут находиться только сами палиндромы: ‘a’, ‘b’, ‘aba’ и так далее. Понятно, что сам палиндромы мы не будем хранить, а просто будем хранить из какой вершины мы пришли сюда, и какой символ с двух сторон добавили. Также у нас существуют две фиктивные вершины для удобной реализации алгоритма.

Но как и в любом интересном дереве, у нас также существуют суффиксные ссылки. Суффиксная ссылка будет вести в вершину, которая также является палиндромом (ну потому что у нас в вершинах хранятся только палиндромы) и которая является наибольшим собственным суффиксом данной вершины. То есть из вершины ‘aba’ ссылка будет вести в вершину ‘a’.

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

Это вкратце, подробнее почитайте информацию по ссылке выше (если не боитесь английского)

Плюсы:

+ Если Вы ранее работали с деревьями, то вам будет очень просто понять данную идею.
+ Позволяет решать большой спектр задач на палиндромы

Минусы:

— Работает медленнее, чем алгоритм Манакера.
— Можно поставить багу. Но, чисто субъективно, тут это сделать сложнее, чем в том же алгоритме Манакера.

Также стоит упомянуть, что с помощью деревьев существует ещё одно решение данной задачи. Оно заключается в использовании суффиксного дерева и быстрого алгоритма LCA(который работает за препроцессинг O(N) и ответ на запрос O(1). Алгоритм Фарах-Колтон-Бендера). Но он на практике не применяется, так как довольно сложен и у него крайне большая скрытая константа. Представляет скорее академический интерес.

Что может быть ещё интересно про алгоритмы? Правильно, потребление памяти и время работы.
На гитхабе Вы можете скачать также код для тестирования, который генерирует случайные строки и ищет в них палиндромы.

Моё тестирование показало, что ожидаемо алгоритм номер 0 работает крайне медленно. Лидером, как Вы можете догадаться, является алгоритм Манакера. Но что самое интересное: алгоритм с O(N^2) выигрывает с примерно двукратным отрывом у алгоритма с использованием хешей с O(NlogN), что ещё раз доказывает, что не асимптотикой единой меряются алгоритмы. Так происходит из-за особенностей отсечения алгоритма номер 1, и отсутствия оной в способе с хешами. Что касается дерева палиндромов, то оно проигрывает Манакеру в основном из-за операций с памятью, так как приходится выделять память под каждую новую вершину. Но если использовать, например, new с размещением, то разрыв сократится.

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

На этом мы завершим наш обзор. Надеюсь, что вы почерпнули для себя хоть что-то новое и вам было хоть чуточку интересно! Все исходники вы можете найти в моём публичном репозитории на Github.

P.S.: Если вы заметили какие-то опечатки, неточности или у вас есть дополнения — прошу отписываться в комментариях и писать в ЛС.
P.P.S.: Смело задавайте вопросы в комментариях — постараюсь ответить, если хватит моей компетенции.

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

0) Что такое палиндром
1) Алгоритм Манакера: Вики, Codeforces, e-maxx
2) Немного про хеши и их применение: e-maxx, Habrahabr
3) Обсуждение про завал хешей на Codeforces: тыц
4) Строки (слова) Туэ-Морса: раз, два
5) Статьи про дерево палиндромов: хорошее описание, codeforces
6) Вот ещё цикл статей про поиск чисел-палиндромов: Хабр

Продолжаем разбирать задачки с сайта Leetcode. В прошлый раз было про массив и сумму чисел, теперь тоже необычное. 

Условия

В переменной X лежит какое-то целое число

Задача — проверить, является ли это число палиндромом.

Задача со звёздочкой — проверить на наличие палиндрома, не используя строки.

Палиндром — это когда строка или число одинаково читается в прямом и обратном направлении:

121 — это палиндром.

А роза упала на лапу Азора — тоже палиндром (если не считать заглавных букв).

12321 — и это палиндром.

Решение, где используем строки

Самый простой способ проверить, число в переменной палиндром или нет, — преобразовать его в строку, выставить знаки задом наперёд и сравнить с оригиналом. Этим мы сразу решаем проблему отрицательных чисел, когда «−121»превращается в «121−» и сразу становится ясно, что это не палиндром.

Сначала решим это на Python. Тут вообще суть в одной строке:

X = 121
if str(X) == str(X)[::-1]:
    print("Это палиндром")
else:
    print("Это не палиндром")

Здесь мы использовали трюк с переворачиванием строки без её изменения — применили конструкцию [::-1]. Работает это так:

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

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

Теперь решим это же, но на JavaScript:

var X = 121;
if (X.toString().split("").reverse().join("") == X.toString()) {
    console.log("Это палиндром")
} else {
    console.log("Это не палиндром")
}

Здесь мы использовали другой метод пересборки:

  1. X.toString() — переводит число в строку.
  2. split(«») — разбивает строку на массив из символов. В кавычках принцип разделения — если бы там была точка, то разделили бы на местах точек. А так как там пустота, то делится вообще по каждому из символов.
  3. reverse() — меняет элементы в массиве в обратном порядке.
  4. join(«») — добавляет результат к пустой строке, чтобы на выходе получить строку в обратном порядке.

Решение без строк

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

Сделаем в JavaScript функцию, которая будет возвращать true, если в переменной лежит палиндром, и false — если нет. Всё остальное будем писать внутри этой функции:

function palindrome(x) {
}

Теперь обработаем три стандартные ситуации:

  1. Если в переменной лежит ноль, то это палиндром.
  2. Если переменная меньше ноля, то это не палиндром.
  3. Если переменная делится на 10 без остатка — это тоже не палиндром.

Запишем это на JavaScript:

function palindrome(x) {
    // если перед нами ноль — это палиндром
    if(x == 0) {
        return true;
    }
    // если число меньше нуля или делится на 10 без остатка — это не палиндром
    if(x < 0 || x%10 == 0){
        return false;
    }
}

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

function palindrome(x) {
    // если перед нами ноль — это палиндром
    if(x == 0) {
        return true;
    }
    // если число меньше нуля или делится на 10 без остатка — это не палиндром
    if(x < 0 || x%10 == 0){
        return false;
    }
    // сюда будем собирать число в обратном порядке
    temp = 0;
    // а тут будем хранить промежуточные значения икса
    preX = x;
    // пока не дойдём до середины числа — повторяем цикл
    while (x > temp) {
        // берём самую правую цифру в числе — это остаток от деления на 10
        pop = x%10;
        // запоминаем старое значение переменной X
        preX = x;
        // и отрезаем от переменной последнюю цифру — делаем это через целую часть деления на 10
        x /= 10;
        // добавляем отрезанную цифру к обратной переменной
        temp = temp*10 + pop;
    }
    // если обратная переменная совпала с оставшейся половиной исходной переменной — это палиндром
    // мы добавляем сравнение с предыдущей версией исходной половины (которая на 1 цифру больше) на тот случай, если исходное число состояло из нечётного количества символов и его нельзя было бы разбить строго пополам
    if(x == temp || preX == temp)
        return true;
    // 
    else
        return false;
};

Для запуска кода просто вызываем функцию и передаём её нашу переменную:

// запускаем код
var X = 121;
console.log(palindrome(X));

Чтобы попрактиковаться, попробуйте сделать такое же, но на Python и не подглядывая в наш код.

Вёрстка:

Кирилл Климентьев

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

Палиндромы

Палиндром — это фраза, которая читается одинаково слева направо и справа налево. Например:

  • «was it a car or a cat I saw?»

  • «а роза упала на лапу Азора»

  • «abacaba» (палиндром нечётной длины)

  • «abba» (палиндром чётной длины)

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

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

Алгоритм Манакера

Пусть есть строка (s) и мы хотим найти в ней все подпалиндромы.

Мы сразу сталкиваемся с очевидной трудностью: их в строке может быть (O(n^2)), что можно видеть на примере строки (s = aa ldots a). Поэтому будем использовать следующий формат: для каждой позиции (s_i) найдём наибольший палиндром, центр которого совпадает с (s_i) (чётные и нечётные палиндромы будем рассматривать отдельно). Половину его длины, округлённую вниз, будем называть радиусом.

Наивное решение — перебрать (s_i), а для него вторым циклом находить наибольшую искомую длину:

vector<int> pal_array(string s) {
    int n = s.size();

    // окружим строку спецсимволами, чтобы не рассматривать выход за границы
    s = "#" + s + "$";

    // в этом массиве будем хранить расстояние от центра до границы палиндрома
    vector<int> t(n, 0);

    for(int i = 1; i <= n; i++)
        while (s[i - t[i - 1]] == s[i + t[i - 1]])
            r[i-1]++;

    return r;
}

Тот же пример (s = aadots a) показывает, что данная реализация работает за (O(n^2)).

Для оптимизации применим идею, знакомую из алгоритма z-функции: при инициализации (t_i) будем пользоваться уже посчитанными (t). А именно, будем поддерживать ((l, r)) — интервал, соответствующий самому правому из найденных подпалиндромов. Тогда мы можем сказать, что часть наибольшего палиндрома с центром в (s_i), которая лежит внутри (s_{l:r}), имеет радиус хотя бы (min(r-i, ; t_{l+r-i})). Первая величина равна длине, дальше которой произошел бы выход за пределы (s_{l:r}), а вторая — значению радиуса в позиции, зеркальной относительно центра палиндрома (s_{l:r}).


vector<int> manacher_odd(string s) {
    int n = (int) s.size();
    vector<int> d(n, 1);
    int l = 0, r = 0;
    for (int i = 1; i < n; i++) {
        if (i < r)
            d[i] = min(r - i + 1, d[l + r - i]);
        while (i - d[i] >= 0 && i + d[i] < n && s[i - d[i]] == s[i + d[i]])
            d[i]++;
        if (i + d[i] - 1 > r)
            l = i - d[i] + 1, r = i + d[i] - 1;
    }
    return d;
}

Так же, как и z-функция, алгоритм работает за линейное время: цикл while запускается только когда (t_i = r — i) (иначе палиндром уже во что-то упёрся), и каждая его итерация сдвигает увеличивает (r) на единицу. Так как (r leq n), получаем, что суммарно эти циклы сделают (O(n)) итераций.

Для случая чётных палиндромов меняется только индексация:

vector<int> manacher_even(string s) {
    int n = (int) s.size();
    vector<int> d(n, 0);
    int l = -1, r = -1;
    for (int i = 0; i < n - 1; i++) {
        if (i < r)
            d[i] = min(r - i, d[l + r - i - 1]);
        while (i - d[i] >= 0 && i + d[i] + 1 < n && s[i - d[i]] == s[i + d[i] + 1])
            d[i]++;
        if (i + d[i] > r)
            l = i - d[i] + 1, r = i + d[i];
    }
    return d;
}

Также можно было не писать отдельно две реализации, а воспользоваться следующим трюком — сделать замену:

[
S = s_1 s_2 dots s_n to S^* = s_1 # s_2 # dots # s_n
]

Теперь нечётные палиндромы с центром в (s_i) соответствуют нечётным палиндромам исходной строки, а нечётные палиндромы с центром в (#) — чётным.

Дерево палиндромов

Дерево палиндромов (англ. palindromic tree, EERTREE) — структура данных, использующая другой, более мощный формат хранения информации обо всех подпалиндромах, чем размеры (n) палиндромов. Она была предложена Михаилом Рубинчиком на летних петрозаводских сборах в 2014-м году.

Лемма. В строке есть не более (n) различных подпалиндромов.

Доказательство. Пусть мы дописываем к строке по одному символу и в данный момент, записав (r) символов, имеем наибольший суффикс-палиндром (s_{l:r}). Пусть у него, в свою очередь, есть суффикс-палиндром (s_{l’:r} = t). Тогда он также имеет более раннее вхождение в строку как (s_{l:l+r-l’} = t). Таким образом, с каждым новым символом у строки появляется не более одного нового палиндрома, и если таковой есть, то это всегда наибольший суффикс-палиндром.

Этот факт позволяет сопоставить всем палиндромам строки сопоставить следующую структуру: возьмём от каждого палиндрома его правую половину (например, (caba) для (abacaba) или (ba) для (abba); будем рассматривать пока что только чётные палиндромы) и добавим все эти половины в префиксное дерево — получившуюся структуру и будем называть деревом палиндромов.

Наивный алгоритм построения будет в худшем случае работать за (O(n^2)), но это можно делать и более эффективно.

Построение за линейное время

Будем поддерживать наибольший суффикс-палиндром. Когда мы будем дописывать очередной символ (c), нужно найти наибольший суффикс этого палиндрома, который может быть дополнен символом (c) — это и будет новый наидлиннейший суффикс-палиндром.

Для этого поступим аналогично алгоритму Ахо-Корасик: будем поддерживать для каждого палиндрома суффиксную ссылку (l(v)), ведущую из (v) в её наибольший суффикс-палиндром. При добавлении очередного символа, будем подниматься по суффиксным ссылкам, пока не найдём вершину, из которой можно совершить нужный переход.

Если в подходящей вершине этого перехода не существовало, то нужно создать новую вершину, и для неё тоже понадобится своя суффиксная ссылка. Чтобы найти её, будем продолжать подниматься по суффиксным ссылкам предыдущего суффикс-палиндрома, пока не найдём второе такое место, которое мы можем дополнить символом (c).

const int maxn = 1e5, k = 26;

int s[maxn], len[maxn], link[maxn], to[maxn][k];

int n, last, sz;

void init() {
    s[n++] = -1;
    link[0] = 1;
    len[1] = -1;
    sz = 2;
}

int get_link(int v) {
    while (s[n-len[v]-2] != s[n-1])
        v = link[v];
    return v;
}

void add_char(int c) {
    s[n++] = c;
    last = get_link(last);
    if (!to[last][c]) {
        len[sz] = len[last] + 2;
        link[sz] = to[get_link(link[last])][c];
        to[last][c] = sz++;
    }
    last = to[last][c];
}

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

Асимптотика

Покажем линейность алгоритма. Рассмотрим длину наибольшего суффикс-палиндрома строки. Каждый новый символ увеличивает её не более, чем на 2. При этом каждый переход по суффиксной ссылке уменьшает её, поэтому нахождение первого суффикс-палиндрома амортизировано работает за линейное время.

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

Содержание

  1. Что такое палиндром
  2. Проверка слова
  3. Проверка фразы

Давайте реализуем алгоритм поиска слов-палиндромов на Java.

Для начала надо определиться, что же такое палиндром? Палиндром – это слово или строка текста, которая читается слева направо и справа налево одинаково. Например, палиндромами являются такие слова как «ага», «дед», «довод», «кабак», «мадам», «шалаш». В более широком смысле число «101» также является палиндромом.

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

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

private boolean isWordPalindrome(String word) {
    var chars = word.toCharArray();
    var left = 0; // индекс первого символа
    var right = chars.length — 1; // индекс последнего символа
    while (left < right) { // пока не дошли до середины слова
        if (chars[left] != chars[right]) {
            return false;
        }
        left++;
        right—;
    }
    return true;
}

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

Затем заводим две переменных left и right для хранения левого и правого индекса соответственно.

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

В самом цикле проверяем равенство левого и правого символа. Если они различны – сразу возвращаем false и выходим из метода. Далее левый символ увеличиваем на 1, а правый – уменьшаем на 1.

Если мы дошли до середины строки и ни разу не встретили различий, то возвращаем true.

Теперь усложним задачу и будем проверять не отдельное слово, а целую фразу. Приведу несколько фраз-палиндромов. Попробуйте прочитать их справа налево.

  • Искать такси
  • Лидер бредил
  • А роза упала на лапу Азора
  • Уж редко рукою окурок держу

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

Взяв за основу ранее рассмотренный метод, немного модифицируем его, преобразуя сначала все символы в нижний регистр:

private boolean isTextPalindrome(String text) {
    var chars = text.toLowerCase().toCharArray();
    var left = 0;
    var right = chars.length — 1;
    while (left < right) {
        if (chars[left] != chars[right]) {
            return false;
        }
        // …

Теперь нам нужно не просто единожды инкрементировать левый индекс, а увеличивать его до тех пор, пока он меньше правого индекса и пока левый символ будет пробелом. Для этого используем цикл с пост-условием do-while:

do {
    left++;
} while (left < right && chars[left] == ‘ ‘);

Аналогично правый индекс уменьшаем до тех пор, пока он больше левого и пока правый символ – пробел:

do {
    right—;
} while (right > left && chars[right] == ‘ ‘);

В итоге наш улучшенный метод примет следующий вид:

private boolean isTextPalindrome(String text) {
    var chars = text.toLowerCase().toCharArray();
    var left = 0;
    var right = chars.length — 1;
    while (left < right) {
        if (chars[left] != chars[right]) {
            return false;
        }

        do {
            left++;
        } while (left < right && chars[left] == ‘ ‘);

        do {
            right—;
        } while (right > left && chars[right] == ‘ ‘);
    }
    return true;
}

Помимо пробелов во фразах могут встречаться и другие символы. Например, знаки препинания. В таком случае, просто замените явную проверку на пробел на вызов метода Character.isLetterOrDigit().

Реализацию данного алгоритма у вас могут спросить на собеседовании, поэтому его полезно знать.

Автор оригинала: Team Python Pool.

Один из самых простых и часто задаваемых вопросов на интервью – проверить, является ли строка палиндромом или нет, используя Python.

Палиндром – это строка или число, которое, если повернуть вспять, равно исходному значению. Например, если мы перевернем строку MALAYALAM, мы получим обратно исходную строку. Кроме того, если мы перевернем число 12321, мы получим 12321 обратно. Они известны как палиндромы.

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

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

  1. Проверьте Палиндром с помощью нарезки (slicing) в Python
  2. Проверьте Палиндром с помощью функции reversed() В Python
  3. Проверьте Палиндром с помощью цикла while в Python
  4. Проверка того, является ли число палиндромом в Python с помощью цикла
  5. Проверка того, является ли фраза палиндромом в Python
  6. Как найти самую длинную палиндромную подстроку в строке

1. Проверьте Palindrome с помощью нарезки в Python

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

def check_palindrome(string):
    # transversing the string from last to first
    reversed_string = string[::-1]
        if string == reversed_string:
            print(string, "is a palindrome")
        else:
            print(string, "is not a Palindrome")
    
if name=='main':
    check_palindrome("RADAR")
    check_palindrome("PythonPool")
Output-
RADAR is a palindrome
PythonPool is not a Palindrome

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

2. Проверьте Палиндром с помощью цикла в Python

def is_palindrome(string):
    reversed_string = ""
    # transversing through string from last
    for i in range(len(string), 0, -1):
        # Addind last characters of string into a new string
        reversed_string += string[i-1]
        if string == reversed_string:
            print("Palindrome")
        else:
            print("Not a palindrome")
            
if __name__ == __'main'__:
    is_palindrome("racecar")
    is_palindrome("Python")
Output-
Palindrome
Not a palindrome

3. Проверьте Палиндром С Помощью функции reversed() в Python

string="MALAYALAM"
# joining characters of reversed string one by one
reversed_string = ''.join(reversed(string))
    if reversed_string == string:
        print(string," is Palindrome")
    else:
        print(string,"Not a Palindrome")
Output-
MALAYALAM is Palindrome

4. Проверьте на Палиндром с помощью цикла while в Python

def check_palindrome(string):
    l = len(string)
    first = 0
    last = l - 1
    isPalindrome = True
    
    # ensuring that we do not iterate through more than half                          #   of the list
    while first < last:
        # if the first character is same as last character keep     #       moving further
        if string[first] == string[last]:
            first = first + 1
            last = last - 1
     # if the characters at first and last do not match break #      the loop
     else:
        isPalindrome = False
        break
        
    return isPalindrome


if __name__ == __'main'__:
    isPalindrome = check_palindrome("MADAM")

    if isPalindrome:
        print("It is a palindrome ")
    else:
        print("Not a Palindrome")
Output-
It is a palindrome

5. Проверка является ли число Палиндромом в Python с помощью цикла

Мы будем использовать следующую концепцию:
Число = 12321
Остаток этого числа, деленный на 10, равен:
Число% 10 = 12321% 10 = 1
Reverse_Number=Remainder=1
Затем мы разделим число на 10.
Число = Number/10 = 12321//10 = 1232
Остаток = 1232% 10 = 2
Reverse_Number=Remainder=12
Число = 1232/10 = 123
Остаток = 123% 10 = 3
Reverse_Number=Remainder=123
Число = 123/10 = 12
Остаток = 12% 10 = 2
Reverse_Number=Remainder=1232
Число = 12/10 = 1
Остаток = 1% 10 = 1
Reverse_Number=Remainder=12321

Программа:

number = 12321
reverse_number = 0
n = number

# while we have not reached the end of the number
while n != 0:
    # finding the last element of number 'n'
    rem = n % 10
    reverse_number = reverse_number * 10 + rem
    n = int(n / 10)
    
if number == reverse_number:
    print("Palindrome")
else:
    print("Not a Palindrome")

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

number = 12321

# converting the number to string and then reversing it
if str(number)[::-1] == str(number):
    print("Number:", number, " is a Palindrome")
else:
    print("Number:", number, " is not a Palindrome")
Output-
Number: 12321 is a Palindrome

6. Проверка того, является ли фраза палиндромом в Python

Проверка, является ли фраза палиндромом или нет, отличается от проверки, является ли слово палиндромом или нет.
Например, фраза “Too hot to hoot” является палиндромом, если игнорировать верхний регистр – нижний регистр и пробелы в символах.

def is_palindrome(string):
    reversed_string = ""
    # Removing all the spaces
    s = string.replace(" ","")
    # making the whole string in lowercase characters 
    s = s.lower()
    for i in range(len(s), 0, -1):
        if s[i-1] >= 'a' and s[i-1] <= 'z':
            reversed_string += s[i - 1]
    if s == reversed_string:
        print("Palindrome")
    else:
        print("Not a palindrome")
        

if __name__ == '__main__':
    is_palindrome("Too hot to hoot")
    is_palindrome("Python")
Output-
Palindrome
Not a palindrome

Есть и другие типы палиндромов, например: “Is it crazy how saying sentences backward creates backward sentences saying how crazy it is”. Он отличается от других палиндромов, которые мы обсуждали до сих пор, потому что здесь, если мы перевернем символы, это не палиндром. Но если мы перевернем его слово за словом, то это будет палиндром.

Программа:

string = 'Is it crazy how saying sentences backwards creates backwards sentences saying how crazy it is'
string1 = string.lower()
string1 = string.replace(" ", "")
new_string = ""

# Wherever there is any space make a list element
list1 = string1.split(" ")

# join the list into string starting from the last
reverse = "".join(list1[::-1])
reverse_string = ""

for i in reverse:
    # adding only characters in the new string
    if i >= 'a' and i <= 'z': 
        reverse_string+=i 

for i in string1:
    if i >= 'a' and i <= 'z':
        new_string += i

if new_string == reverse_string:
    print(string, ":Palindrome")
else:
    print(string, ":Not a Palindrome")
Output-
Is it crazy how saying sentences backward creates backward sentences saying how crazy it is: Palindrome

7. Как найти самую длинную палиндромную подстроку в строке

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

m = ""
s = 'babad'

for i in range(len(s)):
    # iterating through the string from the last
    for j in range(len(s), i, -1):
    # ensuring that we do not iterate through more than half of 
    # the string
        if len(m) >= j-i:
            break
        elif s[i:j] == s[i:j][::-1]:
            m = s[i:j]

print(m)

Алгоритмическая сложность для приведенной выше программы равна O(n^2), так как у нас есть цикл for внутри другого цикла for.

Читайте также:

  • Как преобразовать строку в нижний регистр
  • Как вычислить Квадратный корень
  • Пользовательский ввод | Функция input() | Ввод с клавиатуры

Вывод

Мы изучили, что такое палиндром, как проверить, является ли строка или число палиндромом. Мы также рассмотрели некоторые распространенные вопросы интервью, такие как проверка фразы, если она является палиндромом, и поиск самой длинной подстроки, которая является палиндромом в Python. Я надеюсь, что вы попробуете каждое решение.

Понравилась статья? Поделить с друзьями:
  • Как найти среднее число корня
  • Как найти параграф на клавиатуре компьютера
  • Как найти соцсети человека по адресу
  • Котел ферроли ошибка f05 как исправить
  • Фильм как найти маму казахский