最长回文子串

最长回文子串

  • 我的解法:这道题很快的就写出了答案,唯一不足的是时间复杂度太高了:
def longestPalindrome(s):
    if len(s) <= 1:
        return s
    
    l = list()
    length = len(s)
    
    for i in range(0, length):
        start = i
        j = length-1
        end = j
        while i < j:
            if s[i] == s[j]:
                j -= 1
                i += 1
            else:
                i = start
                j = end - 1
                end = j
        if len(s[start : end+1]) > 1:
            l.append(s[start : end+1])
        #print(l)

    max_index = 0
    for i in range(0, len(l)):
        if len(l[i]) > len(l[max_index]):
            max_index = i
    if len(l) == 0:
        return s[0]
    return l[max_index]


print(longestPalindrome(
	"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
	aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
	aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
	aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
	aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
	aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
	aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
	aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
	aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
	aaaabcaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
	aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
	aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
	aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
	aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
	aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
	aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
	aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
	aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))
  • 对于上面代码的理解,找一个实例在纸上走一遍就知道了。不再赘述。【提示:滑动窗口——类似于:https://blog.csdn.net/jy_z11121/article/details/101381983】
    在这里插入图片描述
  • 这个方法相当于是“遍历法”,可以说几乎任意两个字符都做了比较…
  • 复杂度分析(空间复杂度为1表明只需要常数个变量):
    在这里插入图片描述

在这里插入图片描述

  • 另一种暴力解法:
def isPalindromic(test_s): # 判断test_s字符串是否为回文,是的话则返回True,否则False
	length = len(test_s)
	for i in range(0, int(length/2)):
		if test_s[i] != test_s[length - i - 1]:
			return False
	return True

def longestPalindrome(s):
	longest = 0
	length = len(s)
	for i in range(0, length):
		for j in range(i+1, length+1):
			test_s = s[i:j]
			if isPalindromic(test_s) and len(test_s) > longest:
				ans = test_s
				longest = len(test_s)
	return ans


print(longestPalindrome("aabcddcba"))
  • 思路:从头向尾循环出所有字符串,判断其是否为回文。
  • 此暴力解法与上一解法相比,会更慢一点。
    在这里插入图片描述

改进:最长公共子串

  • 思路:
    因为回文字符串指的是“正着读与倒着读相同”,所以我们可以考虑把原来的字符串倒置,然后找原字符串与倒置后的字符串的最长公共子串。例如 S = “caba” ,S` = “abac”,最长公共子串是 “aba”,所以原字符串的最长回文串就是 “aba”。

  • 实现:

  1. 两个字符串最长公共子序列的找出有多种方法(这也是一个很重要的算法题,如涉及到的KMP算法),这里使用动态规划的方法来找出这是最长公共子序列,也就是原字符串的最长回文序列。
  2. 实现动态规划用到的“备忘录”是一个“一维数组”(列表实现)。这个备忘录要记录什么内容呢?(只要能想清楚这个,就好说了!)这个备忘录应该记录公共子序列的长度。如何实现这一记录过程呢?循环(正向循环)每个原字符串,其循环体里将循环(逆向循环)倒置后的字符串,使其分别与原字符串中的每个字符进行比较,根据每次的比较结果(两字符是否相等)来更新这个备忘录。为什么需要逆向循环呢?因为我们需要用到上一轮的结果(arr[j] = arr[j-1] + 1)!如果正向循环的话,我们就无法使用上一轮的更新结果了,使用到的将是本轮新更新的结果。
  • 这个备忘录实际上具有两个意义,一是记录原字符串中的字符s[i-1]与倒置字符串的每个字符比较的结果,二是记录原字符串中的字符s[i]与倒置字符串的每个字符比较的结果。当要更新i(即i即将进行一次i++操作时)时,此时备忘录里,只有“原字符串中的字符s[i-1]与倒置字符串的每个字符比较的结果”;当同一轮i下,仅仅只是在更新j时,则备忘录里既含有“原字符串中的字符s[i-1]与倒置字符串的每个字符比较的结果”,也含有“原字符串中的字符s[i]与倒置字符串的每个字符比较的结果”。请参考下图。
    在这里插入图片描述
    在这里插入图片描述
  1. 然而,这种方法来寻找回文序列是有缺陷的。如:当 S = "abc435cba"时 ,S` = “abc534cba”,此时,将判断出最长公共子序列为“abc”和“cba”,但它们在原字符串里并不是回文序列!所以,我们应有所判断。那么,如何排除这种情况呢?答案就是再进行一个判断:如果判断出来的公共子序列真的是原字符串里的回文序列,那么一定有一些规矩,具体请看代码吧
  2. 代码如下:
def longestPalindrome(s):
	length = len(s)
	arr = [0] * length # 创建“备忘录”
	#print(arr)
	rev_s = ''.join(reversed(s)) # s的倒置字符串
	#print(rev_s)
	
	maxLen = 0
	maxEnd = 0
	
	for i in range(0, length):
		for j in range(length-1, -1, -1):
			if s[i] == rev_s[j]:
				if i == 0 or j == 0:
					arr[j] = 1
				else:
					arr[j] = arr[j-1] + 1
			else:
				arr[j] = 0
				
			# 判断是否真的是回文序列
			if arr[j] > maxLen:
				beforeRev = length - j - 1 # 该字符在原字符串中的索引
				if beforeRev + arr[j] - 1 == i:
					maxLen = arr[j]
					maxEnd = i
					
	return s[maxEnd-maxLen+1 : maxEnd+1]


print(longestPalindrome("abad"))

碎碎念:对于我来说,不好理解,也更不好想到,花了大概两个小时才差不多想明白…好复杂,而且也没什么太强的规则性。。。学着了解怎么样的情况下需逆向循环吧。

改进:动态规划

def longestPalindrome(s):
	if len(s) <= 1:
		return s

	length = len(s)
	l = list() 		# 列表l用来存放所有回文

	# 初始化一字母和二字母的回文
	for i in range(0, length):
		l.append([i, i])
		if i + 1 <= length - 1:
			l.append([i, i+1])
	#print(l)

	# 根据一字母和二字母的回文找到三字母回文、四字母回文...
	for i in range(0, length):
		for j in range(0, length):
			if s[i] == s[j] and ([i+1, j-1] in l):
				l.append([i, j])
	# print(l)

	end_l = l[len(l)-1]
	if end_l[1] - end_l[0] == 0: # 最长回文就是相邻两字符的情况
		for xl in l:
			if xl[0] != xl[1] and s[xl[0]] == s[xl[1]] : # 相邻两字符真的是回文的情况
				ret_l = xl
				break
			else: # 所有相邻两字符都并不是回文的情况——返回s的首字符
				ret_l = l[0]
	else:
		# 找出最长回文
		max_l = l[0]
		for xl in l:
			if xl[1]-xl[0] > max_l[1]-max_l[0]:
				max_l = xl
		ret_l = max_l

	return s[ret_l[0] : ret_l[1]+1]	
	
	
print(longestPalindrome("abcba"))

在这里插入图片描述
可以看到,上面的运行结果是错误的,因为“根据一字母和二字母的回文找到三字母回文、四字母回文…”这一块的代码有问题,应该“从内向外”找,否则会先判断最外侧的两个“a”是否符合回文关系,根据条件,列表 l 中还没有能使它得出“是回文”结果的内容,所以就没有被保存下来。然而,就算“从内向外”找,对于这个for循环来说,它也并不认得在这个for循环里append()的内容,仍然是按照最开始找到的一二字母的回文来执行for循环代码块中。。。就不知道如何解决了。。。

但是可以使用C或Java的二维数组来实现,具体请看:https://leetcode-cn.com/problems/longest-palindromic-substring/solution/xiang-xi-tong-su-de-si-lu-fen-xi-duo-jie-fa-bao-gu/

改进:中心扩展

  • 思路:
    我们知道回文串一定是对称的,所以我们可以每次选择一个中心,进行左右扩展,判断左右字符是否相等即可。由于回文序列不是奇数个就是偶数个,因此“中心扩展”的"中心"既可能是一个字符(如“aba”的中心是“a”),也可能是一个假象的分割线(如“aaaa”的中心)。也因此,对于给定的一个字符串来说,总共有 [ 2 ∗ l e n ( S ) − 1 ] [2*len(S)-1] [2len(S)1] 个中心。
    在这里插入图片描述
  • 实现:
def expandAroundCenter(s, left, right):
	L, R = left, right
	while L >= 0 and R < len(s) and s[L] == s[R]:
		L -= 1
		R += 1
	return R - L - 1

def longestPalindrome(s):
	if len(s) <= 1:
		return s

	start, end = 0, 0
	length = len(s)
	for i in range(0, length):
		len1 =  expandAroundCenter(s, i, i) # 以一个字符为中心进行扩展
		len2 =  expandAroundCenter(s, i, i) # 以假象的分割线为中心进行扩展
		longest = max(len1, len2)
		if longest > end - start:
			start = i - (longest-1) // 2
			end = i + longest // 2
	return s[start : end+1]
	
	
print(longestPalindrome("abcba"))

碎碎念:不管是思路还是代码都挺好理解的,但是并不容易去想出它的代码实现…

改进:Manacher’s Algorithm 马拉车算法

Manacher’s Algorithm由一个叫 Manacher 的人在 1975 年发明的,这个方法的最大贡献是将此问题的时间复杂度降到了线性 O ( n ) O(n) O(n)。下面进行该算法的讲解:

思路
  1. 对原字符串S进行处理
  • 为了使得扩展的过程中,到边界后自动结束,在两端分别插入 “^” 和 “$”——两个不可能在字符串中出现、且不相同的字符。这样中心扩展的时候,判断到边界的两端字符是否相等的时,答案是否,从而退出循环。
  • 在原字符串的每两个字符间插入字符“#”。假设原Sabcbaade,则进行上面的两个处理后,得到新的字符串T
    在这里插入图片描述

进行了上面两个操作后,就可以保证所给字符串的长度一定是奇数了。为什么要作如此“保证”呢? 因为这样就可以不论原字符串是奇数个字符还是偶数个字符,都可以统一进行处理了,原字符串中的每个字符都能进行后面需要的计算。

  1. 构造一个有意义的数组P
  • 依次以字符串T中的每个字符作为中心进行扩展,用一个数组P保存最大扩展出的有意义的字符个数(“有意义的字符”指的是原字符串S中的字符)。

示例:对于下图中的字符串T——
T[0]对应的P为1,因为只有"#c#"
T[6]对应的P为5,因为扩展得到的内容是"#c#b#c#b#c"
T[11]对应的P为2,因为扩展得到的内容是“#c#c#”
在这里插入图片描述

  1. 得出最终结果——最大回文子串
  • 数组P中元素值最大的那个,就是我们想要的回文子串。例如,这里的P[6]最大,所以我们希望返回“cbcbc”。规律在于:用 P 的下标 i 减去 P [ i ],再除以 2,就是回文子串的首字符下标了。例如我们找到 P[ i ] 的最大值为 5,也就是回文串的最大长度是 5,对应的下标是 6,所以原字符串的开头下标是(6 - 5 )/ 2 = 0。所以我们只需要返回原字符串的第 0 到 第(5 - 1)位就可以了。
求数组P

这是算法的关键,它充分利用了回文串的对称性,从而降低了时间复杂度。

在上面的思路讲解时的第二步,我们知道了数组P的存在。那么,如何将其用程序求出来呢?

如果用上面已经讲到的中心扩展法,那就向两边扩展比对就行了,但是这样的话这个算法就没有啥意义了,因为就相当于上面的中心扩展法了,反而还搞得复杂了一点。所以,我们这里有新的idea——利用回文的对称性

  • 想象你在"abaaba"中心画一道竖线,你是否注意到这个字符串围绕此竖线是中心对称的?再试试"aba"的中心,这个字符串围绕此中心也是对称的。这当然不是巧合,而是在某个条件下的必然规律。我们将利用此规律减少对数组P中某些元素的重复计算。
  • 找一个字符串中的最长回文子串时,最坏的情况是各个回文相互重叠的时候(例如"aaaaaaaaaa"和"cabcbabcbabcba"),这是因为这种情况会发生重复计算。换句话说,没有重叠时,只能一点一点计算,也就没有可改进的余地了。
  • 于是我们就可以考虑:花费一些空间来避免重复计算利用回文的特性避免重复计算

假设有一个具有重叠因子的字符串S: babcbabcbaccba,对其进行转换得到字符串T:
在这里插入图片描述C 表示回文串回文"abcbabcba"的中心,L、R 分别表示回文串的左右边界。假设你已经算出了一部分 P,你下一步要计算P[i],i 围绕 C 的对称点是 i’ 。你有办法高效地计算 P[i] 吗?

我们先看一下 i 围绕 C 的对称点 i’(此时 i’ = 9):
在这里插入图片描述
据上图所示,很明显 P[i] = P[i’] = 1。这是因为 i 和 i’ 围绕 C 对称。同理,P[12] = P[10] = 0P[14] = P[8] = 0

然而,P[15] != (P[7] = 7)(P[15] = 5)!这是为什么呢?
在这里插入图片描述
如上图所示,两条绿色实线划定的范围必定是对称的,两条绿色虚线划定的范围必定也是对称的。此时请注意 P[i’] = 7,然后我们找到以 P[i] 为中心的长度为7的回文串,发现这个范围超过了左边界L(超出的部分不再对称)。根据这个回文串所占的绿色区域,我们能认为 P[i] >= 5,至于 P[i] > 5 是否成立,还要通过后面的(绿色范围外的)逐个字符检测才能判定出来。在此例中,我们判断到 P[21] ≠ P[9],所以P[i] = P[15] = 5。

对于上面的讲述作总结,即:

if P[ i' ] < R – i :
	P[ i ] ← P[ i' ]
else:
	P[ i ] ≥ R - i  (此时要从R开始逐个字符求P[i],并更新C及其R)

很明显,C的位置也需要变动。它的变化规律是:如果i处的回文超过了R,那么就C=i,同时相应改变L和R即可。

对此算法的完整流程进行总结(PS. 下面的说过程应该比对着代码以及自己做出图示来理解):

  • P 从索引 i 为 1 处开始求,并初始化 C、R 都为 0;
  • 每一轮都应该先求出 i’ ,即 i 关于 C 的对称索引(在代码中用 i_mirror 来表示):i_mirror = C - (i - C) ;
  • 每一次也还都要判断当前要求的 P 的索引 i 是否出了对称区域,即 i > R 的话,就出了区域——我们用 diff 表示 (R - i):
  1. 如果 diff >= 0 的话, 就说明仍在对称区域内,则可以利用回文串的对称性来求 P[i] : 根据上面讲到的,这里还不能直接另 P[i] = P[i_mirror],因为可能 P[i_mirror] 的回文长度超出了我们设定在与 R 有关的那个范围内,这种情况下,继续比对超过对称范围外的 P[i] 两侧的字符,并改变 C 和 R。
  2. 如果 diff < 0 的话,就说明 i > R,说明前面没判断到对称的部分,所以 P[i] = 0,同时继续判断其两边的字符是否相等,如果相等,则 ++P[i],同时改变C和R。
  • 最后从 P[i] 里选择出最大值,作出相应截取就行了。
def preProcess(s):
	length = len(s)
	if length == 0:
		return "^$"

	ret = "^"
	for i in range(length):
		ret += '#' + s[i]
	ret += "#$"
	return ret


def longestPalindrome(s):
	t = preProcess(s)
	length = len(t)
	p = [-1] * length
	c, r = 0, 0

	for i in range(1, length-1):
		i_mirror = c - (i - c)
		diff = r - i
		if diff >= 0: # 当前i在C和R之间,可以利用回文的对称属性
			if p[i_mirror] < diff: # i的对称点的回文长度在C的大回文范围内部
				p[i] = p[i_mirror]
			else:
				p[i] = diff
				# i处的回文可能超出C的大回文范围了
				while t[i + p[i] + 1] == t[i - p[i] - 1]:
					p[i] += 1
					c = i
					r = i + p[i]
		else:
			p[i] = 0
			while t[i + p[i] + 1] == t[i - p[i] - 1]:
				p[i] += 1
				c = i
				r = i + p[i]

	maxLen = 0
	centerIndex = 0
	for i in range(1, length-1):
		if p[i] > maxLen:
			maxLen = p[i]
			centerIndex = i

	return s[(centerIndex-1-maxLen)//2 : (centerIndex-1-maxLen)//2+maxLen]


print(longestPalindrome("babcbabcbaccba"))
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值