添加最少量的字符以构成回文

时间:2013-09-11 03:10:24

标签: algorithm palindrome

问题:

  

给定任何字符串,添加尽可能少的字符,使其在线性时间内成为回文。

我只能提出O(N 2 )解决方案。

有人可以帮我解决O(N)问题吗?

11 个答案:

答案 0 :(得分:4)

  1. 还原字符串
  2. 使用修改后的Knuth-Morris-Pratt查找最新匹配(最简单的修改是将原始字符串附加到恢复的字符串,并忽略len(字符串)后的匹配。
  3. 将已恢复字符串的不匹配其余部分附加到原始字符串。
  4. 1和3显然是线性的,2是线性的,因为Knuth-Morris-Pratt是。

答案 1 :(得分:4)

如果仅允许追加

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)是(b,b)好的,无事可做,这对很好。让我们增加柜台。
  2. 对(1,1)是(l,o)。不匹配。所以,让我们添加" o"在左边的位置1。现在我们的话变成了" bolob"。
  3. 配对(2,2)。我们不需要查看字符,因为我们指向字符串中的相同索引。完成。
  4. 等一下,但我们在这里有一个问题:在第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)最坏情况,因此您可能不想全部枚举它们。)

答案 2 :(得分:2)

我认为@Chronical的答案是错误的,因为它似乎是最佳案例场景,而不是用于计算big-O的最坏情况复杂。我欢迎这个证据,但是&#34;解决方案&#34;并没有真正描述一个有效的答案。

KMP在O(n * 2k)时间内找到匹配的子字符串,其中n是输入字符串的长度,k我们正在搜索的子字符串,但不在{ {1}}时间告诉你输入字符串中的最长回文是什么。

要解决这个问题,我们需要在字符串末尾找到最长的回文。如果此最长后缀回文的长度为O(n),则要添加的最小字符数为x。例如。字符串n - x的最长后缀子字符串是aaba,长度为aba,因此我们的答案为3。无论是使用KMP还是更有效和简单的算法(1),查找字符串是否为回文结构的算法都需要O(n)时间:

  

取两个指针,一个在第一个角色,另一个在最后一个角色

     

比较指针上的字符,如果它们相等,则向内移动每个指针,否则返回O(n/2)

     

当指针指向相同的索引(奇数字符串长度)或重叠(偶数字符串长度)时,返回false

使用简单的算法,我们从整个字符串开始,检查它是否是回文。如果是,我们会返回true,如果没有,我们会检查字符串0string[1...end],直到我们找到一个字符并返回string[2...end]。这导致运行时为n - 1

将KMP算法分解为

  

构建表

     

搜索最长后缀回文

构建表需要O(n^2)次,然后每次检查&#34;你是一个回文&#34;对于来自O(n)的每个子字符串,每个字符串需要string[0...end], string[1...end], ..., string[end - 2...end]次。在这种情况下,O(n)与简单算法检查每个子字符串的k因子相同,因为它以n开头,然后经过k = n,{{1 ......就像简单算法一样。

TL; DR:

KMP可以在k = n - 1时间告诉你字符串是否是回文,但是这提供了问题的答案,因为你必须检查所有子字符串k = n - 2是否是回文,导致相同(但实际上更糟)运行时作为简单的回文检查算法。

答案 3 :(得分:1)

#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,则代码中的所有内容都会相反;即哪个索引在每次迭代时递增,并在找到匹配时递增打印回文的顺序等等。我不想要经历它又一次,但你可以尝试看看。

此代码的运行复杂性应为 O(N) ,假设std::string类的append方法在固定时间内运行。

答案 4 :(得分:1)

O(n)时间解决方案。 算法: 需要在给定字符串中找到包含最后一个字符的最长回文。然后以相反的顺序将不属于回文部分的所有字符添加到字符串的后面。

关键点: 在这个问题中,给定字符串中最长的回文必须包含最后一个字符。

例如: 输入:abacac 输出:abacacaba 这里输入中包含最后一个字母的最长回文是&#34; cac&#34;。因此,在&#34; cac&#34;之前添加所有字母。向后以相反的顺序使整个弦成为回文。

用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);

    }
}

请注意,此代码将为您提供最少量的字母到APPEND TO THE BACK以使字符串成为回文的解决方案。如果你想要附加到前面,只需要另一个方向的第二个循环。这将使算法O(n)+ O(n)= O(n)。如果你想要一种方法在字符串中的任何地方插入字母以使其成为回文,那么这段代码将不适用于那种情况。

答案 5 :(得分:1)

如果有人想在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

答案 6 :(得分:0)

我假设您无法替换或删除任何现有字符?

一个良好的开端是反转其中一个字符串并在反向字符串和另一个字符串之间找到最长公共子字符串(LCS)。既然听起来这是一个家庭作业或面试问题,我会把剩下的留给你。

答案 7 :(得分:0)

这里看到这个解决方案 这比O(N ^ 2)更好 问题分为许多其他子问题 例如:
原创&#34; tostotor&#34;
逆转&#34; rototsot&#34;
这里的第二个位置是&#39; o&#39;通过闯入&#34; t&#34;分成两个问题。和&#34; ostot&#34;从原始字符串
对于&#39;:解决方案是1
对于&#39; ostot&#39;:解决方案是2 因为 LCS是&#34; tot&#34; 并且需要添加的字符是&# 34; OS&#34;
所以总数 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")

答案 8 :(得分:0)

  

使用递归

#include <iostream>
using namespace std;

int length( char str[])
{  int l=0;
  for( int i=0; str[i]!='\0'; 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);
  }

答案 9 :(得分:0)

时间复杂度为O(n)的解决方案

public static void main(String[] args) {

    String givenStr = "abtb";
    String palindromeStr = covertToPalindrome(givenStr);
    System.out.println(palindromeStr);

}

private static String covertToPalindrome(String str) {

    char[] strArray = str.toCharArray();

    int low = 0;
    int high = strArray.length - 1;
    int subStrIndex = -1;

    while (low < high) {
        if (strArray[low] == strArray[high]) {
            high--;
        } else {
            high = strArray.length - 1;
            subStrIndex = low;
        }
        low++;
    }

    return str +  (new StringBuilder(str.substring(0, subStrIndex+1))).reverse().toString();

}

答案 10 :(得分:0)

//要附加的字符串以将其转换为回文

public static void main(String args[])
{
  String s=input();
  System.out.println(min_operations(s));
}

static String min_operations(String str)
{

  int i=0;
  int j=str.length()-1;
  String ans="";

  while(i<j)
  {
   if(str.charAt(i)!=str.charAt(j))
   {
    ans=ans+str.charAt(i);
   }
   if(str.charAt(i)==str.charAt(j))
   {
      j--;
   }

    i++;
}

 StringBuffer sd=new StringBuffer(ans);
 sd.reverse();
 return (sd.toString());

}