Добавьте наименьшее количество символов для создания палиндрома

вопрос:

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

Я могу придумать только O (N2) решение.

может кто-нибудь помочь мне с решением O(N)?

9 ответов


  1. возвращает строку
  2. используйте измененный Кнут-Моррис-Пратт чтобы найти последнее совпадение(простейшей модификацией было бы просто добавить исходную строку к возвращенной строке и игнорировать совпадения после LEN (string).
  3. добавьте непревзойденную остальную часть возвращенной строки к оригиналу.

1 и 3, очевидно, линейны, а 2-линейны, потому что кнут-Моррис-Пратт.


если только добавление разрешено

решение Scala:

def isPalindrome(s: String) = s.view.reverse == s.view

def makePalindrome(s: String) = 
  s + s.take((0 to s.length).find(i => isPalindrome(s.substring(i))).get).reverse

Если вам разрешено вставлять символы в любом месте

каждый палиндром можно рассматривать как набор вложенных пар письме.

a  n  n  a         b  o  b
|  |  |  |         |  *  |
|   --   |         |     |
---------           -----

если длина палиндрома N Четна, у нас будет n/2 пары. Если это нечетно, у нас будет n/2 полных пары и одна буква в середине (назовем ее вырожденной парой).

давайте представим их парами строковых индексов - левый индекс отсчитывается от левого конца строки, а правый индекс отсчитывается от правого конца строки, причем оба конца начинаются с индекса 0.

теперь давайте напишем пары, начиная от внешнего к внутреннему. Итак, в нашем примере:

anna: (0, 0) (1, 1)
bob: (0, 0) (1, 1)

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

пример: Предположим, что входное слово "blob"

  1. пара (0, 0) is (b, b) ok, ничего не делать, эта пара в порядке. Давайте увеличим счетчик.
  2. пара (1, 1) is (l, o). Не совпадает. Итак, давайте добавим " o " в позицию 1 слева. Теперь наше слово стало "bolob".
  3. Пара (2, 2). Нам не нужно смотреть даже на символы, потому что мы указываем на один и тот же индекс в строке. Сделанный.

подождите, но у нас есть проблема здесь: в точке 2. мы произвольно решили добавить символ слева. Но мы могли бы также добавить символ " l " справа. Это произведет "blolb", также действительный палиндром. Так это имеет значение? К сожалению, это так, потому что выбор в предыдущих шагах может повлиять на то, сколько пар нам придется исправить и, следовательно, сколько символов нам придется добавить в будущих шагах.

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

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

(0, 0) (1, 2)
(0, 0) (2, 1)

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

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

после каждого шага, мы удалим Парето-неоптимальным последовательности. Последовательность субоптимальна по сравнению с другой последовательностью той же длины, если ее последняя пара доминировал последней парой другой последовательности. Е. Г. последовательность (0, 0)(1, 3) хуже (0, 0)(1, 2). Последнее дает нам больше места для поиска вложенных пар, и мы гарантированно найдем по крайней мере все пары, которые мы найдем для первого. Однако последовательность (0, 0)(1, 2) не хуже и не лучше чем (0, 0)(2, 1). Одна незначительная деталь, которую мы должны остерегаться, заключается в том, что последовательность, заканчивающаяся вырожденной парой, всегда хуже, чем последовательность, заканчивающаяся полной парой.

после объединения всех вместе:

def makePalindrome(str: String): String = {

  /** Finds the pareto-minimum subset of a set of points (here pair of indices).
    * Could be done in linear time, without sorting, but O(n log n) is not that bad ;) */
  def paretoMin(points: Iterable[(Int, Int)]): List[(Int, Int)] = {
    val sorted = points.toSeq.sortBy(identity)
    (List.empty[(Int, Int)] /: sorted) { (result, e) =>
      if (result.isEmpty || e._2 <= result.head._2)
        e :: result
      else
        result
    }
  }

  /** Find all pairs directly nested within a given pair.
    * For performance reasons tries to not include suboptimal pairs (pairs nested in any of the pairs also in the result)
    * although it wouldn't break anything as prune takes care of this. */
  def pairs(left: Int, right: Int): Iterable[(Int, Int)] = {
    val builder = List.newBuilder[(Int, Int)]
    var rightMax = str.length
    for (i <- left until (str.length - right)) {
      rightMax = math.min(str.length - left, rightMax)
      val subPairs =
        for (j <- right until rightMax if str(i) == str(str.length - j - 1)) yield (i, j)

      subPairs.headOption match {
        case Some((a, b)) => rightMax = b; builder += ((a, b))
        case None =>
      }
    }

    builder.result()
  }

  /** Builds sequences of size n+1 from sequence of size n */
  def extend(path: List[(Int, Int)]): Iterable[List[(Int, Int)]] =
    for (p <- pairs(path.head._1 + 1, path.head._2 + 1)) yield p :: path

  /** Whether full or degenerated. Full-pairs save us 2 characters, degenerated save us only 1. */
  def isFullPair(pair: (Int, Int)) =
    pair._1 + pair._2 < str.length - 1

  /** Removes pareto-suboptimal sequences */
  def prune(sequences: List[List[(Int, Int)]]): List[List[(Int, Int)]] = {
    val allowedHeads = paretoMin(sequences.map(_.head)).toSet
    val containsFullPair = allowedHeads.exists(isFullPair)
    sequences.filter(s => allowedHeads.contains(s.head) && (isFullPair(s.head) || !containsFullPair))
  }

  /** Dynamic-Programming step */
  @tailrec
  def search(sequences: List[List[(Int, Int)]]): List[List[(Int, Int)]] = {
    val nextStage = prune(sequences.flatMap(extend))
    nextStage match {
      case List() => sequences
      case x => search(nextStage)
    }
  }

  /** Converts a sequence of nested pairs to a palindrome */
  def sequenceToString(sequence: List[(Int, Int)]): String = {
    val lStr = str
    val rStr = str.reverse

    val half =
      (for (List(start, end) <- sequence.reverse.sliding(2)) yield
        lStr.substring(start._1 + 1, end._1) + rStr.substring(start._2 + 1, end._2) + lStr(end._1)).mkString

    if (isFullPair(sequence.head))
      half + half.reverse
    else
      half + half.reverse.substring(1)
  }

  sequenceToString(search(List(List((-1, -1)))).head)
}

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


я считаю, что ответ @Chronical неверен, как и кажется, для лучший сценарий, а не в худшем случае который используется для вычисления сложности big-O. Я приветствую доказательство, но "решение" на самом деле не описывает действительный ответ.

KMP находит соответствующую подстроку в O(n * 2k), где n - длина входной строки, и k подстрока, которую мы ищем, но не в O(n) время сказать вам, что длинной палиндром во входной строке есть.

чтобы решить эту проблему, нам нужно найти самый длинный палиндром в конце строки. Если этот самый длинный суффикс палиндром имеет длину x минимальное количество символов, чтобы добавить n - x. Е. Г. строка aabaсамая длинная подстрока суффикс aba длиной 3, таким образом, наш ответ 1. Алгоритм, чтобы узнать, является ли строка палиндромом, принимает O(n) время, использует ли KMP или больше эффективный и простой алгоритм (O(n/2)):

Возьмите два указателя, один на первом символе и один на последнем символе

сравните символы в указателях, если они равны, переместите каждый указатель внутрь, иначе return false

когда указатели указывают на один и тот же индекс (нечетная длина строки) или перекрываются (четная длина строки), верните true

используя простой алгоритм, мы начинаем из всей строки и проверьте, если это палиндром. Если это так, мы возвращаемся 0, а если нет, то проверяем строку string[1...end], string[2...end] пока мы не достигли одного символа и возврата n - 1. Это приводит к выполнению O(n^2).

разделение алгоритма KMP на

построить таблицу

поиск самого длинного суффикса палиндрома

построение таблицы занимает O(n) время, а затем каждая проверка "являются вы палиндром " для каждой подстроки из string[0...end], string[1...end], ..., string[end - 2...end] каждый O(n) времени. k в этом случае тот же коэффициент n что простой алгоритм проверки каждой подстроки, потому что он начинается с k = n, затем через k = n - 1, k = n - 2... так же, как и простой алгоритм.

TL; DR:

KMP может сказать вам, является ли строка палиндромом в O(n) время, но это дает ответ на вопрос, потому что вы должны проверить, все ли подстроки string[0...end], string[1...end], ..., string[end - 2...end] являются палиндромами, что приводит к тому же (но на самом деле хуже) времени выполнения как простой алгоритм проверки палиндрома.


#include<iostream>
#include<string>

using std::cout;
using std::endl;
using std::cin;

int main() {

    std::string word, left("");
    cin >> word;
    size_t start, end;

    for (start = 0, end = word.length()-1; start < end; end--) {
        if (word[start] != word[end]) { 
            left.append(word.begin()+end, 1 + word.begin()+end);
            continue;
        }
        left.append(word.begin()+start, 1 + word.begin()+start), start++;
    }
    cout << left << ( start == end ? std::string(word.begin()+end, 1 + word.begin()+end) : "" ) 
        << std::string(left.rbegin(), left.rend()) << endl;
    return 0;
}

не знаю, добавляет ли он минимальное число, но он производит палиндромы

пояснил:

  • мы начнем с обоих концов данной строки и будем итерировать внутрь к центру.
  • на каждой итерации мы проверяем, одинакова ли каждая буква, Т. е. word[start] ==word[end]?.

    • если они одинаковы, мы добавляем копию переменной word[start] в другую строку, называемую left который, как следует из названия, будет служить левой стороной новой строки палиндрома, когда итерация будет завершена. Затем мы увеличиваем обе переменные (start)++ и (end)-- к центру
    • в случае, если они не совпадают, то добавляем копию переменной word[end] к той же строке left
  • и это основы алгоритма, пока цикл не будет выполнен.

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

Примечание что если вы решите добавить oppoosite символов в строку left, наоборот обо всем, в коде становится истинным, т. е. индекс увеличивается на каждой итерации и, который увеличивается, когда найдено совпадение, порядок печать палиндрома, etc. Я не хочу проходить через это снова, но вы можете попробовать и посмотреть.

сложность выполнения этого кода должна быть O (N) предполагая, что метод добавления std::string класс бежит в постоянное время.


о(n) времени. Алгоритм: Необходимо найти самый длинный палиндром в данной строке, содержащий последний символ. Затем добавьте все символы, которые не являются частью палиндрома, в конец строки в обратном порядке.

ключевой момент: В этой задаче самый длинный палиндром в данной строке должен содержать последний символ.

ex: вход: abacac выход: abacacaba Здесь самый длинный палиндром во входных данных, содержащий последнюю букву "ККА." Поэтому добавьте всю букву перед " cac " назад в обратном порядке, чтобы сделать всю строку палиндромом.

написано на c# с несколькими тестовыми случаями, прокомментированными

 static public void makePalindrome()
    {
        //string word = "aababaa";
        //string word = "abacbaa";
        //string word = "abcbd";
        //string word = "abacac";
        //string word = "aBxyxBxBxyxB";
        //string word = "Malayal";
        string word = "abccadac";

        int j = word.Length - 1;
        int mark = j;
        bool found = false;

        for (int i = 0; i < j; i++)
        {
            char cI = word[i];
            char cJ = word[j];

            if (cI == cJ)
            {
                found = true;
                j--;
                if(mark > i)
                    mark = i;
            }
            else
            {
                if (found)
                {
                    found = false;
                    i--;
                }
                j = word.Length - 1;
                mark = j;
            }
        }

        for (int i = mark-1; i >=0; i--)
            word += word[i];

        Console.Write(word);

    }
}

обратите внимание, что этот код даст вам решение для наименьшего количества букв, добавляемых к спине, чтобы сделать строку палиндромом. Если вы хотите добавить на фронт, только 2-й цикл, который идет в другую сторону. Это сделает алгоритм O(n) + O(n) = O (n). Если вы хотите вставьте Буквы в любом месте строки, чтобы сделать ее палиндромом, тогда этот код не будет работать для этого случая.


Если некоторые хотят решить это в ruby, решение может быть очень простым

str = 'xcbc' # Any string that you want.
arr1 = str.split('')
arr2 = arr1.reverse
count = 0

while(str != str.reverse)
  count += 1
  arr1.insert(count-1, arr2[count-1])
  str = arr1.join('')
end

puts str
puts str.length - arr2.count

Я предполагаю, что вы не можете заменить или удалить существующие символы?

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


здесь см. Это решение Это лучше, чем O(N^2)
Проблема подразделяется на многие другие подзадачи
ex:
оригинал " tostotor"
реверсивный " rototsot"
Здесь 2-я позиция " o "так делится на две задачи, разбивая на" t " и "ostot" из исходной строки
Для 't':Решение 1
Для "ostot":решение 2 , потому что LCS - это "тот" и символы должны быть добавлены "os"
таким образом, всего 2+1 = 3

def  shortPalin( S):
    k=0
    lis=len(S)
        for i in range(len(S)/2):
        if S[i]==S[lis-1-i]:
            k=k+1
        else :break
    S=S[k:lis-k]
    lis=len(S)
    prev=0
    w=len(S)
    tot=0
    for i in range(len(S)):
        if i>=w:
            break;
        elif S[i]==S[lis-1-i]:
             tot=tot+lcs(S[prev:i])
             prev=i
             w=lis-1-i
    tot=tot+lcs(S[prev:i])
    return tot

def  lcs( S):
    if (len(S)==1):
        return 1
    li=len(S)
    X=[0 for x in xrange(len(S)+1)]
    Y=[0 for l in xrange(len(S)+1)]
    for i in range(len(S)-1,-1,-1):
        for j in range(len(S)-1,-1,-1):
            if S[i]==S[li-1-j]:
                X[j]=1+Y[j+1]
            else:
                X[j]=max(Y[j],X[j+1])
        Y=X
    return li-X[0]
print shortPalin("tostotor")

С Помощью Рекурсии

#include <iostream>
using namespace std;

int length( char str[])
{  int l=0;
  for( int i=0; str[i]!=''; i++, l++);
return l;
}
int palin(char str[],int len)
{  static int cnt;
   int s=0;
   int e=len-1;
   while(s<e){
    if(str[s]!=str[e]) {
            cnt++;
         return palin(str+1,len-1);}
    else{
         s++;
         e--;
     }
  }
return cnt;
}
  int main() {
    char str[100];
    cin.getline(str,100);
    int len = length(str);
    cout<<palin(str,len);
  }