LeetCode题解——Longest Palindromic Substring

转自:http://blog.csdn.net/hopeztm/article/details/7932245

题目: 

Given a string S, find the longest palindromic substring in S.

给出一个字符串S,找到一个最长的连续回文串。


例如串 babcbabcbaccba 最长回文是:abcbabcba

这个题目小弟给出3中解法,前两种的都是 O(n^2), 第三种思路是O(n). 


思路1. 动态规划

这里动态规划的思路是 dp[i][j] 表示的是 从i 到 j 的字串,是否是回文串。

则根据回文的规则我们可以知道:

如果s[i] == s[j] 那么是否是回文决定于 dp[i+1][ j - 1]

当 s[i] != s[j] 的时候, dp[i][j] 直接就是 false。

动态规划的进行是按照字符串的长度从1 到 n推进的。

代码很明晰:给出java代码,复杂度 O(n^2)

[java]  view plain copy
  1. public class DPSolution {  
  2.   
  3.     boolean[][] dp;  
  4.       
  5.     public String longestPalindrome(String s)  
  6.     {  
  7.         if(s.length() == 0)  
  8.         {  
  9.             return "";  
  10.         }  
  11.         if(s.length() == 1)  
  12.         {  
  13.             return s;  
  14.         }  
  15.   
  16.         dp = new boolean[s.length()][s.length()];  
  17.           
  18.         int i,j;  
  19.           
  20.         for( i = 0; i < s.length(); i++)  
  21.         {  
  22.             for( j = 0; j < s.length(); j++)  
  23.             {  
  24.                 if(i >= j)  
  25.                 {  
  26.                     dp[i][j] = true//当i == j 的时候,只有一个字符的字符串; 当 i > j 认为是空串,也是回文  
  27.   
  28.                 }  
  29.                 else  
  30.                 {  
  31.                     dp[i][j] = false//其他情况都初始化成不是回文  
  32.                 }  
  33.             }  
  34.         }  
  35.           
  36.         int k;  
  37.         int maxLen = 1;  
  38.         int rf = 0, rt = 0;  
  39.         for( k = 1; k < s.length(); k++)  
  40.         {  
  41.             for( i = 0;  k + i < s.length(); i++)  
  42.             {  
  43.                 j = i + k;  
  44.                 if(s.charAt(i) != s.charAt(j)) //对字符串 s[i....j] 如果 s[i] != s[j] 那么不是回文  
  45.                 {  
  46.                     dp[i][j] = false;  
  47.                 }  
  48.                 else  //如果s[i] == s[j] 回文性质由 s[i+1][j-1] 决定  
  49.                 {  
  50.                     dp[i][j] = dp[i+1][j-1];  
  51.                     if(dp[i][j])  
  52.                     {  
  53.                         if(k + 1 > maxLen)  
  54.                         {  
  55.                             maxLen = k + 1;  
  56.                             rf = i;  
  57.                             rt = j;  
  58.                         }  
  59.                     }  
  60.                 }  
  61.             }  
  62.         }  
  63.         return s.substring(rf, rt+1);  
  64.     }  
  65. }  



思路2. KMP匹配

第二个思路来源于字符串匹配,最长回文串有如下性质: 

对于串S, 假设它的 Reverse是 S', 那么S的最长回文串是 S 和 S' 的最长公共字串。

例如 S = abcddca,  S' = acddcba, S和S'的最长公共字串是 cddc 也是S的最长回文字串。

如果S‘是 模式串,我们可以对S’的所有后缀枚举(S0, S1, S2, Sn) 然后用每个后缀和S匹配,寻找最长的匹配前缀。

例如当前枚举是 S0 = acddcba 最长匹配前缀是 a

S1  = cddcba 最长匹配前缀是 cddc

S2 = ddcba 最长匹配前缀是 ddc

当然这个过程可以做适当剪枝,如果当前枚举的后缀长度,小于当前找到的最长匹配,则直接跳过。


Java 代码如下:

[java]  view plain copy
  1. public class Solution {  
  2.     private int[] next;  
  3.     private void GetNext(String s) //KMP求next数组  
  4.     {  
  5.         int i,j;  
  6.           
  7.         i = 0;   
  8.         j = -1;  
  9.           
  10.         next[0] = -1;  
  11.           
  12.         while( i < s.length())  
  13.         {  
  14.             if( j == -1 || s.charAt(i) == s.charAt(j))  
  15.             {  
  16.                 i++;  
  17.                 j++;  
  18.                 next[i] = j;  
  19.             }  
  20.             else  
  21.             {  
  22.                 j = next[j];  
  23.             }  
  24.         }  
  25.     }  
  26.     private int compare(String pattern, String s) //用KMP算法做求出最长的前缀匹配  
  27.     {  
  28.         int i,j;  
  29.           
  30.         i = 0;  
  31.         j = 0;  
  32.        
  33.         int maxLen = 0;  
  34.         while( i < s.length())  
  35.         {  
  36.             if(j == -1 || pattern.charAt(j) == s.charAt(i))  
  37.             {  
  38.                 i++;  
  39.                 j++;  
  40.             }  
  41.             else  
  42.             {  
  43.                 j = next[j];  
  44.             }  
  45.             if( j > maxLen)  
  46.             {  
  47.                 maxLen = j;  
  48.             }  
  49.             if(j == pattern.length())  
  50.             {  
  51.                 return maxLen;  
  52.             }  
  53.         }  
  54.         return maxLen;  
  55.     }  
  56.       
  57.     public String longestPalindrome(String s)  //  
  58.     {  
  59.         // Start typing your Java solution below  
  60.         // DO NOT write main() function  
  61.         String reverString = new StringBuilder(s).reverse().toString();  //求得到 输入string 的reverse  
  62.         next = new int[s.length() + 1];  
  63.           
  64.         String maxPal = "";  
  65.         int maxLen = 0;  
  66.         int len;  
  67.         for(int i = 0; i < s.length(); i++) //枚举所有后缀  
  68.         {  
  69.             String suffix = reverString.substring(i);  
  70.             if(suffix.length() < maxLen)  
  71.             {  
  72.                 break;  
  73.             }  
  74.             GetNext(suffix);  
  75.             len = compare(suffix, s);  
  76.             if( len > maxLen)  
  77.             {  
  78.                 maxPal = suffix.substring(0, len);  
  79.                 maxLen = len;  
  80.             }  
  81.               
  82.         }  
  83.         return maxPal;  
  84.           
  85.     }  
  86. }  



思路3. 思路来源于此

http://www.leetcode.com/2011/11/longest-palindromic-substring-part-ii.html

不过原文的陈述仔细研究了一下,有一些地方让人着实费解,所以自己决定重写一遍。

这里描述了一个叫Manacher’s Algorithm的算法。

算法首先将输入字符串S, 转换成一个特殊字符串T,转换的原则就是将S的开头结尾以及每两个相邻的字符之间加入一个特殊的字符,例如#

例如: S = “abaaba”, T = “#a#b#a#a#b#a#”.

为了找到最长的回文字串,例如我们当前考虑以Ti为回文串中间的元素,如果要找到最长回文字串,我们要从当前的Ti扩展使得 Ti-d … Ti+d 组成最长回文字串. 这里 d 其实和 以Ti为中心的回文串长度是一样的. 进一步解释就是说,因为我们这里插入了 # 符号,对于一个长度为偶数的回文串,他应该是以#做为中心的,然后向两边扩,对于长度是奇数的回文串,它应该是以一个普通字符作为中心的。通过使用#,我们将无论是奇数还是偶数的回文串,都变成了一个以Ti为中心,d为半径两个方向扩展的问题。并且d就是回文串的长度。

例如 #a#b#a#, P = 0103010, 对于b而言P的值是3,是最左边的#,也是延伸的最左边。这个值和当前的回文串是一致的。

如果我们求出所有的P值,那么显然我们要的回文串,就是以最大P值为中心的回文串。

T = # a # b # a # a # b # a #
P = 0 1 0 3 0 1 6 1 0 3 0 1 0

例如上面的例子,最长回文是 “abaaba”, P6 = 6.

根据观察发现,如果我们在一个位置例如 abaaba的中间位置,用一个竖线分开,两侧的P值是对称的。当然这个性质不是在任何时候都会成立,接下来就是分析如何利用这个性质,使得我们可以少算很多P的值。

下面的例子 S = “babcbabcbaccba” 存在更多的折叠回文字串。


C表示当前的回文中心,L和R处的线表示以C为中心可以到达的最左和最右位置,如果知道这些,我们如何可以更好的计算C后面的P[i]. 
假设我们当前计算的是 i = 13, 根据对称性,我们知道对称的那个下标 i' = 9. 

根据C对称的原则,我们很容易得到如下数据  P[ 12 ] = P[ 10 ] = 0, P[ 13 ] = P[ 9 ] = 1, P[ 14 ] = P[ 8 ] = 0).

Now we are at index i = 15, and its mirrored index around C is i’ = 7. Is P[ 15 ] = P[ 7 ] = 7?

当时当i = 15的时候,却只能得到回文 “a#b#c#b#a”, 长度是5, 而对称 i ' = 7 的长度是7. 


如上图所示,如果以 i, i' 为中心,画出对称的区域如图,其中以i‘ = 7 对称的区域是 实心绿色 + 虚绿色 和 左侧,虚绿色表示当前的对称长度已经超过之前的对称中心C。而之前的P对称性质成立的原因是 i 右侧剩余的长度 R - i 正好比 以 i‘ 为中心的回文小。 
这个性质可以这样归纳,对于 i 而言,因为根据C对称的最右是R,所以i的右侧有 R - i 个元素是保证是 i' 左侧是对称的。 而对于 i' 而言他的P值,也就是回文串的长度,可能会比 R-i 要大。 如果大于 R - i, 对于i而言,我们只能暂时的先填写 P[i] = R - i, 然后依据回文的属性来扩充P[i] 的值; 如果P[i '] 小于R-i,那么说明在对称区间C内,i的回文串长度和i' 是一样长的。例如我们的例子中 i = 15, 因为R = 20,所以i右侧 在对称区间剩余的是 R - 15 = 5, 而 i’ = 7 的长度是7. 说明 i' 的回文长度已经超出对称区间。我们只能使得P[i] 赋值为5, 然后尝试扩充P[i]. 
if P[ i' ] ≤ R – i,
then P[ i ] ← P[ i' ]
else P[ i ] ≥ R – i. (这里下一步操作是扩充 P[ i ].

扩充P[i] 之后,我们还要做一件事情是更新 R 和 C, 如果当前对称中心的最右延伸大于R,我们就更新C和R。在迭代的过程中,我们试探i的时候,如果P[i'] <= R - i, 那么只要做一件事情。 如果不成立我们对当前P[i] 做扩展,因为最大长度是n,扩展最多就做n次,所以最多做2*n。 所以最后算法复杂度是 O(n)

或许贴上代码更容易一些。直接使用大神的代码了,虽然自己也实现了,不过是理解大神的思路实现的。

[cpp]  view plain copy
  1. // Transform S into T.  
  2. // For example, S = "abba", T = "^#a#b#b#a#$".  
  3. // ^ and $ signs are sentinels appended to each end to avoid bounds checking  
  4. string preProcess(string s) {  
  5.   int n = s.length();  
  6.   if (n == 0) return "^$";  
  7.   string ret = "^";  
  8.   for (int i = 0; i < n; i++)  
  9.     ret += "#" + s.substr(i, 1);  
  10.    
  11.   ret += "#$";  
  12.   return ret;  
  13. }  
  14.    
  15. string longestPalindrome(string s) {  
  16.   string T = preProcess(s);  
  17.   int n = T.length();  
  18.   int *P = new int[n];  
  19.   int C = 0, R = 0;  
  20.   for (int i = 1; i < n-1; i++) {  
  21.     int i_mirror = 2*C-i; // equals to i' = C - (i-C)  
  22.    
  23.     P[i] = (R > i) ? min(R-i, P[i_mirror]) : 0;  
  24.    
  25.     // Attempt to expand palindrome centered at i  
  26.     while (T[i + 1 + P[i]] == T[i - 1 - P[i]])  
  27.       P[i]++;  
  28.    
  29.     // If palindrome centered at i expand past R,  
  30.     // adjust center based on expanded palindrome.  
  31.     if (i + P[i] > R) {  
  32.       C = i;  
  33.       R = i + P[i];  
  34.     }  
  35.   }  
  36.    
  37.   // Find the maximum element in P.  
  38.   int maxLen = 0;  
  39.   int centerIndex = 0;  
  40.   for (int i = 1; i < n-1; i++) {  
  41.     if (P[i] > maxLen) {  
  42.       maxLen = P[i];  
  43.       centerIndex = i;  
  44.     }  
  45.   }  
  46.   delete[] P;  
  47.    
  48.   return s.substr((centerIndex - 1 - maxLen)/2, maxLen);  
  49. }  

你可能感兴趣的:(LeetCode,String,回文字符串)