Leetcode算法——5、最长回文子串

题目

Given a string s, find the longest palindromic substring in s. You may assume that the maximum length of s is 1000.

给定一个字符串s,找到s中最长的回文字字符串。假设s的最大长度为1000.

示例:
Example 1:

Input: "babad"
Output: "bab"
Note: "aba" is also a valid answer.

Example 2:

Input: "cbbd"
Output: "bb"

思路

1、暴力求解

扫描所有子字符串,分别判断是否为回文字符串,返回最长的回文字符串。
时间复杂度为O(n^3)。

2、暴力求解的优化

每次判断一个字符串是否是回文字符串时,p(s[i:j]) = p(s[i+1:j-1]) && s[i] == s[j]。
将每次的判断结果存存起来,之后再用就不用重新计算了。
但需要从后向前遍历,这样才会用得到提前存储的结果。
时间复杂度为O(n^2)。

3、求原字符串和逆字符串的公共子串

1、求公共子串可以使用动态规划方法,详情见find_max_same_substring.py
2、不是所有的公共子串都符合要求,还需要判断原字符串翻转之后是否还是同一个子串。
时间复杂度为O(n^2)。

4、移动中心法

先假设某个位置为回文字符串的中心,然后查询以此位中心的最长回文字符串。遍历中心,即可找到全局最长子串。
时间复杂度为O(n^2)。

5、马拉车算法

Manacher发明出来的。时间复杂度为O(n)。详见下篇。

python实现

# -*- coding: utf-8 -*-

def longestPalindrome(s):
    """ :type s: str :rtype: str 暴力求解。扫描所有子字符串,分别判断是否为回文字符串,返回最长的回文字符串。 时间复杂度为O(n^3) """

    def is_palindromic(a):
        ''' a是否为回文字符串 '''
        b = a[::-1]
        if a == b:
            return True
        return False

    max_substr = ''
    for i in range(len(s)):
        for j in range(i, len(s)):
            substr = s[i:j+1]
            if is_palindromic(substr) and len(substr) > len(max_substr):
                max_substr = substr
    return max_substr

def longestPalindrome2(s):
    """ :type s: str :rtype: str 暴力求解的优化:每次判断一个字符串是否是回文字符串时,p(s[i:j]) = p(s[i+1:j-1]) && s[i] == s[j]。 将每次的判断结果存存起来,之后再用就不用重新计算了。 但需要从后向前遍历,这样才会用得到提前存储的结果。 时间复杂度为O(n^2)。 """

    if len(s) <= 1:
        return s

    def is_palindromic(a, dic):
        ''' a是否为回文字符串 '''
        result = False
        if len(a) == 1:
            result = True
        elif len(a) == 2:
            result = a[0] == a[1]
        else:
            b = a[1:-1]
            if b in dic:
                result = dic[b] and a[0] == a[-1]
            else:
                b = a[::-1]
                if a == b:
                    result = True
        dic[a] = result
        return result

    max_substr = ''
    dic = dict()
    for i in range(len(s)-1, -1, -1):
        for j in range(i, len(s)):
            substr = s[i:j+1]
            if is_palindromic(substr, dic) and len(substr) > len(max_substr):
                max_substr = substr
    return max_substr

def longestPalindrome3(s):
    """ :type s: str :rtype: str 求原字符串和逆字符串的公共子串。 1、求公共子串可以使用动态规划方法,详情见 https://github.com/HappyRocky/pythonAI/blob/master/algorithm-exercise/find_max_same_substring.py 2、不是所有的公共子串都符合要求,还需要判断原字符串翻转之后是否还是同一个子串。 时间复杂度为O(n^2)。 """
    if len(s) <= 1:
        return s

    # 翻转
    s1 = s
    s2 = s[::-1]

    # 矩阵,高为s1长度,宽为s2长度
    flag_matrix = []
    max_len = 0
    last_idx = 0
    for i in range(len(s1)):
        flag_matrix.append([])
        for j in range(len(s2)):
            if s1[i] == s2[j]:
                if i > 0 and j > 0:
                    flag_matrix[i].append(flag_matrix[i-1][j-1] + 1)
                else:
                    flag_matrix[i].append(1)
            else:
                flag_matrix[i].append(0)
            # 比已知的最大长度还长,且:s1中子串起始位置距离原字符串的起始位置 == s2中子串的末尾位置距离原字符串的末尾位置 
            if flag_matrix[i][j] > max_len and i - flag_matrix[i][j] + 1 == len(s2) - j - 1:
                max_len = max(max_len, flag_matrix[i][j])
                last_idx = i
    return s1[last_idx-max_len+1 : last_idx+1]

def longestPalindrome4(s):
    """ :type s: str :rtype: str 移动中心法。 先假设某个位置为回文字符串的中心,然后查询以此位中心的最长回文字符串。遍历中心,即可找到全局最长子串。 时间复杂度为O(n^2)。 """
    if len(s) <= 1:
        return s

    max_str = ''

    # 枚举mid_idx。一个mid_id决定了两个中心位置:mid_idx本身、和右邻之间的分界线。
    for mid_idx in range(len(s)):

        # 以mid_idx为中心
        count = 0  # 从中心向两边慢慢扩张
        while(True):
            start_idx = mid_idx-count
            end_idx = mid_idx+count
            if s[start_idx] == s[end_idx]:
                count += 1
                if end_idx - start_idx + 1 > len(max_str):
                    max_str = s[start_idx:end_idx+1]
            else:
                break
            if start_idx == 0 or end_idx == len(s)-1:
                break

        # 以和右邻之间的分界线为中心
        if mid_idx == len(s)-1: # 最后一个元素,没有右邻,直接跳过
            continue
        count = 0
        while(True):
            start_idx = mid_idx - count
            end_idx = mid_idx + count + 1
            if s[start_idx] == s[end_idx]:
                count += 1
                if end_idx - start_idx + 1 > len(max_str):
                    max_str = s[start_idx:end_idx+1]
            else:
                break
            if start_idx == 0 or end_idx == len(s)-1:
                break      

    return max_str

def longestPalindrome5(s):
    """ :type s: str :rtype: str 马拉车算法。Manacher发明出来的。 时间复杂度为O(n)。 """
    if len(s) <= 1:
        return s

    # 每个字符之间插入 \1
    ss = '\0\1' + '\1'.join([x for x in s]) + '\1\2'
    p = [0] * len(ss)
    center = 0
    mx = 0
    max_str = ''
    for i in range(1, len(p)-1):

        # 初始化 p[i]
        if i > mx:
            p[i] = 0
        else:
            j = 2 * center - i # i 关于 center 的对称点
            p[i] = min(mx-i, p[j])

        # 尝试继续向两边扩展,更新 p[i]
        try:
            while ss[i - p[i] - 1] == ss[i + p[i] + 1]: # 不必判断是否溢出,因为首位均有特殊字符,肯定会退出
                p[i] += 1
        except:
            print(i + p[i] + 1)

        # 更新中心
        if i + p[i] > mx:
            mx = i + p[i]
            center = i

        # 更新最长串
        if 1 + 2 * p[i] > len(max_str):
            max_str = ss[i - p[i] : i + p[i] + 1]

    return max_str.replace('\1', '')


if '__main__' == __name__:
    s_list = ["babad","cbbd"]
    for s in s_list:
        result = longestPalindrome5(s)
        print(f'{s}\t{result}')

点赞