5. Longest Palindromic Substring

题目:

Given a string S, find the longest palindromic substring in S. You may assume that the maximum length of S is 1000, and there exists one unique longest palindromic substring.

链接:https://leetcode.com/problems/longest-palindromic-substring/

题解:

卡在这道题很久,直接导致没有继续刷题的动力。这道题有许多种解法,下面分别来看一看最简单的中心展开法, Manacher算法,后缀树suffix tree,以及使用Rabin-carp rolling hash方法

1) 中心展开法:

从头遍历数组,考虑Palindrome是奇数和偶数两种情况。  Time Complexity - O(n2),  Space Complexity - O(1)

public class Solution {
    public String longestPalindrome(String s) {     // O(n * n)  - go from middle
        if(s == null || s.length() == 0)
            return "";
        String res = s.substring(0, 1);
        
        for(int i = 0; i < s.length(); i++) {
            String tmp = getSubstring(s, i, i);
            if(tmp.length() > res.length()) 
                res = tmp;
            tmp = getSubstring(s, i, i + 1);
            if(tmp.length() > res.length()) 
                res = tmp;
        }        
        return res;
    }
    
    private String getSubstring(String s, int lo, int hi) {
        while(lo >= 0 && hi <= s.length() - 1 && s.charAt(lo) == s.charAt(hi)) {
                lo--;
                hi++;
        }
        return s.substring(lo + 1, hi);
    }
}

 

2) Manacher's Algorithm

非常天才的方法,充分利用了Palindrome的特性,代码绝大部分使用了Sedgewick教授的。           Time Complexity - O(n), Space Complexity - O(n)

  1. 先对原字符串等距离插入'#',可以略去奇偶Panlindrome的判定
  2. 以当前center中心设置 i 的mirror,假如 i 仍然在当前center最长Palindrome的右边界范围内,依据Palindrome的特性, p[i] = Math.min(right - i, p[mirror])
  3. 以i为中心进行扩展,计算出p[i] - 当前的最长Palindrome
  4. 假如以i为中心的最长Palindrome超过了右边界,更新i和right
public class Solution {              //mostly coded by Sedgewick
    private int[] p; 
    private String s;
    private char[] t;                //transformed string
    
    public String longestPalindrome(String s) {
        this.s = s;
        preprocess();
        p = new int[t.length];
        
        int center = 0, right = 0;        
        for(int i = 1; i < t.length - 1; i++) {
            int mirror = 2 * center - i;
            if(right > i)                       
                p[i] = Math.min(right - i, p[mirror]);
                
            while(i - p[i] >= 0 && i + p[i] < p.length && t[i + p[i]] == t[i - p[i]])     //try to expand
                p[i]++;
            
            if(i + p[i] > right){
                center = i;
                right = i + p[i];
            }
        }
        
        int maxLength = 0;   
        center = 0;   
        
        for (int i = 1; i < p.length-1; i++) { // i is center,  p[i] is half length of longest Palindrome in t
            if (p[i] > maxLength) {
                maxLength = p[i];
                center = i;
            }
        }
        
        return s.substring((center - p[center] + 2) / 2, (center + p[center]) / 2); 
    }
    
    private void preprocess() {                 //preprocess to avoid old/even length
        t = new char[s.length() * 2 + 1];
        for(int i = 0; i < s.length(); i++) {
            t[2 * i] = '#';
            t[2 * i + 1] = s.charAt(i);
        }

        t[t.length - 1] = '#';
    }
}

 

3) Suffix Tree

后缀树/后缀词典/后缀数组应该是这类问题的终极解决方法了。对于这个问题后缀树不如Manacher有效率。不过我们还是来看一看怎么解决这个问题。 (待补充)

 

4) Rabin-carp, rolling hash

 

 

二刷:

Java:   Manacher:  Time Complexity - O(n), Space Complexity - O(n)  - 1/3/2016

二刷的时候仍然被这算法卡了几天。今天再次尝试叙述一下逻辑:

  1. preprocess,将输入字符串s等间距插入特殊字符'#',目的是为了以后计算方便,不用太多考虑长度的奇偶性,不preprocess其实也可以
  2. char[] t代表经过preprocess以后的transformed string
  3. int[] p, p[i]是length of longest Palindromic string centerred at i,就是以i为中心,最长Palindromic string的长度
  4. mirror代表以当前的center为中心,坐标i在center左边的镜像。因为 i - center = center - mirror,所以mirror = 2 * center - i。
  5. 下面比较重要的一点就是Manacher's Algorithm最主要的性质, 这里利用了之前计算过的最长的Palindromic String。假设之前计算过一个很长很长的Palindromic String,其中心在center, 那么我们遍历当前center的后面的元素 i 的时候,假如这个i在之前那个很长的Palindromic String的右边界范围内,那么我们就可以得到一个公式来节约重新匹配 - p[i] = Math.min(p[mirror], right - i), 即 p[i] 的当前最小值等于 p[mirror] 和 right - i这两个值中的较小者。 这里p[mirror]是以这个mirror为中心的最长回文字符串的长度。
    1. 就比如"#a#b#a#a#b#a#",假如当前的center在两个"a"中间的"#",假设我们正计算以之后那个"a"为中心的结果, 因为之前的p[mirror]等于2,所以这里p[i]至少等于2和right - i中的较小者。
    2. 假如我们正计算第二个"b"为中心的结果,因为之前的p[mirror]等于4,所以p[i]至少等于4和right - i中的较小者。
    3. 有了p[mirror]和right - i这两个边界,我们就可以节约许多的重复matching
  6. 上一步确定了p[i]的最小值以后,我们就可以继续进行尝试扩展当前Palindromic。这个时候我们没有取巧的办法,只能在t中对t[i + p[i]]和t[i - p[i]]进行逐字符对比,假如他们相同,则我们增加p[i]的长度,直到求出以i为中心的最长回文串长度
  7. 在上一步找出了以i为中心的最长回文串长度后,我们比较一下当前字符串的右边界是否大于历史最长回文串右边界"right",假如当前回文串更长,则我们更新center = i,  新的右边界right = p[i] + i。
  8. 这里我们就完成了p[i]的计算,也就是我们得到了每个以i为中心的最长回文字符串的长度。 这个时候我们再遍历一遍数组,找到其中最长的那一个,然后再对原始字符串进行substring操作就可以了
public class Solution {
    public String longestPalindrome(String s) {
        if (s == null || s.length() == 0) {
            return s;
        }
        char[] t = new char[s.length() * 2 + 1];    // transformed string
        int[] p = new int[t.length];      // p[i] is length of longest Palindromic string centered at i
        preprocess(s, t);
        int center = 0, right = 0;
        
        for (int i = 1; i < t.length - 1; i++) {
            int mirror = 2 * center - i;    //   center - mirror = i - center,  mirror is i's mirror based on center
            if (i < right) {                // if i within pre-calculated palindrome boundary
                p[i] = Math.min(p[mirror], right - i);  //then p[i] is at least p[mirror]
            }                                                            // or right - i
            while (i + p[i] < t.length && i - p[i] >= 0 && t[i + p[i]] == t[i - p[i]]) {   // try to expand current Palindrome
                    p[i]++;    
            }
            if (i + p[i] > right) {// if new palindromic string right boundary > old boundary, update center and right boundary
                center = i;
                right = i + p[i];
            }
        }
        
        center = 0;
        int maxLen = 0;
        for (int i = 1; i < p.length - 1; i++) {
            if (p[i] > maxLen) {
                center = i;
                maxLen = p[i];
            }
        }
        
        return s.substring((center - p[center] + 2) / 2, (center + p[center]) / 2);
    }
    
    private void preprocess(String s, char[] t) {
        for (int i = 0; i < s.length(); i++) {
            t[2 * i] = '#';
            t[2 * i + 1] = s.charAt(i);
        }
        t[t.length - 1] = '#';
    }
}

 

Python:

class Solution(object):
    def longestPalindrome(self, s):
        """
        :type s: str
        :rtype: str
        """
        t = '#'
        for char in s:                  # preprocess
            t += char + '#'
        p = [0] * len(t)                # p[i] is longest Palindromic string centered at i
        center = right = 0
        for i in range(1, len(t) - 1):
            mirror = 2 * center - i     # mirror is mirror of i centered at center,  center - mirror = i - center
            if i < right:           # if i within range of lps, i is at least min(p[mirror], right - i)
                p[i] = min(p[mirror], right - i)    
            while i + p[i] < len(t) and i - p[i] >= 0 and t[i - p[i]] == t[i + p[i]]:   # try expand current palindromic string
                p[i] += 1
            if i + p[i] > right:                    # try update right
                center = i
                right = i + p[i]
        center = 0
        maxLen = 0
        for i in range(1, len(t) - 1):          # find the longest one
            if p[i] > maxLen:
                center = i
                maxLen = p[i]
        return s[(center - p[center] + 2) / 2 : (center + p[center]) / 2]                        

 

 

Reference:

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

http://algs4.cs.princeton.edu/home/

http://en.wikipedia.org/wiki/Longest_palindromic_substring

http://www.felix021.com/blog/read.php?2040                         <- 中文

http://stackoverflow.com/questions/10468208/manachers-algorithm-algorithm-to-find-longest-palindrome-substring-in-linear-t

posted @ 2015-04-15 20:26  YRB  阅读(519)  评论(0编辑  收藏  举报