Manacher算法解决在一个字符串中最长回文子串问题
1 回文问题
1.1 暴力解
没有实轴碰虚轴的情况
加#号辅助找回文
但时间复杂度为O(N方)
1.2 Manacher算法
1.2.1 理论
- 回文半径、直径、区域
- 回文半径数组pArr[ ]
- 回文最右边界(int R)
- 取得R更新的回文中心点(int C)
比如
#1#2#2#1#…
- R = -1 C = -1 ,初始在-1位置,开始以0为中心向两边扩,比-1大,所以R=0,C是当R更新时,是哪个点让R更新的,此时是以0为中心更新的R,所以C=0. R = 0 C = 0
以1为中心向两边扩,1左边位置0为#,右边位置2为#,R取到了更右边的边界,R = 2,谁让R更新的,是1位置,所以C=1
只要变得更右了,R就更新,C就更新
大前提:i在R内,i撇是对称位置
有三种情况
第一大情况:
i位置在R外,可以用暴力做
第二大情况:i位置在R内
- 第一种情况,i的对称点(i撇)回文区域彻底在L…R内部
- 第二种情况,i的对称点 i撇回文区域有一部分超过LR了
- 第三种情况:i的对称点i撇的回文区域左边界压线
i到R距离和i撇的回文半径一样
https://www.bilibili.com/video/BV1FR4y117UQ/?spm_id_from=333.337.search-card.all.click&vd_source=8094924cd2ccfafb6ee1359567f94439
1.2.2 伪代码
# manacher算法,返回回文串中最长的长度
def maxPLen(s):
# "12321" -> "#1#2#3#2#1#"
s -> s2
pArr = [] 长度为s2长度
R = -1
C = -1
for i in range(len(s2)):
if i在R外:
暴力做 成功了,R会变大
else i在R内:
if i的对称点回文区域彻底在L...R内部:
pArr[i] = pArr[i的对称点] 这已经是确认答案了
elif i的对称点回文区域跑到L...R外部:
pArr[i] = i...R的距离 也已经确定了距离
else i的对称点回文区域左边界和L压线:
从R外开始扩 R内不扩,i一定关于i自己是回文
外扩成功了,向右扩,R会变大
pArr[i] = ?
return pArr最大值/2 -> 就是原始字符串回文长度,返回
1.2.3 代码
def manacher(s):
if s is None or len(s) == 0:
return 0
str = manacherString(s) # 中间加 # 字符 12321 -> #1#2#3#2#1#
# pArr数组中记录回文半径大小
pArr = [0] * len(str) # 长度为sx的长度
C = -1
R = -1 # R代表最右的扩成功的位置,这里小修改成了,是失败的第一个位置,而之前都是成功的。中:最右的扩成功位置的,再下一个位置
Max = float("-inf") # 记录所有回文半径数组的最大值
# 每个i位置从左往右依次扩,每个位置求答案
for i in range(len(str)): # 每个位置求一次最大半径
# 13-26行很顺利的把pArr求完了
# 这就是所有情况
# R是第一个违规的位置,刚才的笔记是最后一个成功的位置,稍改一下,i >= R 表示i在R外
# i位置扩出来的答案,i位置扩的区域,至少是多大
# i不用验的区域是1,R>i 表示i在R内, 2 * C - i 是对称点
# min(pArr[2 * C - i],R - i) i撇区域大小和R...L的距离谁小,谁是不用验证的区域
pArr[i] = min(pArr[2 * C - i],R - i) if R > i else 1 # 4种情况都在这句了,不在盒内直接赋值1
while i + pArr[i] < len(str) and i - pArr[i] > -1: # 能否继续再扩
# 情况一跑这个没问题的
if str[i + pArr[i]] == str[i - pArr[i]]:
pArr[i] += 1 # 这个位置的半径扩1
else:
break
# 有没有刷新更右得边界
if i + pArr[i] > R:
R = i + pArr[i] # R更新右边界
C = i # C是使R更新的中心
Max = max(Max,pArr[i])
return Max - 1
def manacherString(s):
charArr = list(s)
res = []
index = 0
for i in range(len(charArr)*2+1):
if i % 2 == 0:
res.append('#')
else:
res.append(charArr[index])
index += 1
return res
a = "btasa"
print(manacher(a))
1.2.3 相关题目
一个字符串,将字符串变成整体都是回文串,只能在字符串后边添加字符,问在最后添加字符串最短是多少才能变成回文串?
比如:abc12321->abc12321cba 把abc逆序添加到1后面,而且这个1是回文半径的最后一个数,把最左的中心求出来,保证回文串最长。
比如:ab121312141213121
一定要求最左的中心
如何改写Manacher:
1.manacher算法求完,哪一个i位置把最后一个位置包住了,就停
2.不用求完manacher算法,某一个位置扩到最后一个位置就停
修改一些代码,第38行,数值关系如下
def manacher(s):
if s is None or len(s) == 0:
return 0
str = manacherString(s) # 中间加 # 字符 12321 -> #1#2#3#2#1#
# pArr数组中记录回文半径大小
pArr = [0] * len(str) # 长度为sx的长度
C = -1
R = -1 # R代表最右的扩成功的位置,这里小修改成了,是失败的第一个位置,而之前都是成功的。中:最右的扩成功位置的,再下一个位置
maxContainsEnd = -1 # 记录所有回文半径数组的最大值
# 每个i位置从左往右依次扩,每个位置求答案
for i in range(len(str)):
# 13-26行很顺利的把pArr求完了
# 这就是所有情况
# R是第一个违规的位置,刚才的笔记是最后一个成功的位置,稍改一下,i >= R 表示i在R外
# i位置扩出来的答案,i位置扩的区域,至少是多大
# i不用验的区域是1,R>i 表示i在R内, 2 * C - i 是对称点
# min(pArr[2 * C - i],R - i) i撇区域大小和R...L的距离谁小,谁是不用验证的区域
pArr[i] = min(pArr[2 * C - i],R - i) if R > i else 1 # 4种情况都在这句了
while i + pArr[i] < len(str) and i - pArr[i] > -1:
# 情况一跑这个没问题的
if str[i + pArr[i]] == str[i - pArr[i]]:
pArr[i] += 1
else:
break
# 有没有刷新更右得边界
if i + pArr[i] > R:
R = i + pArr[i] # R更新右边界
C = i # C是使R更新的中心
if R == len(str): # 若R到了最右边界,就停
maxContainsEnd = pArr[i] # 提取回文半径,pArr没有求完,跳出来
break
res = [0] * (len(s) - maxContainsEnd + 1)
for i in range(len(res)):
res[len(res) - 1 - i] = s[i]
return ''.join(res)
def manacherString(s):
charArr = list(s)
res = [0] * (len(s) * 2 + 1)
index = 0
for i in range(len(res)):
res.append('#' if (i & 1) == 0 else charArr[index])
index += 1 if (i & 1) == 1 else 0
return res
a = "abb12321"
print(manacher(a))
2 Morris遍历
关于树遍历,时间复杂度O(N),空间复杂度O(1)
流程:
当前节点cur,一开始cur来到整棵树头部
- cur无左树,cur = cur.right
- cur有左树,找到左树最右节点,记为mostright
(1)如果mostright的右指针是指向None的,mostright.right指向当前节点,cur = cur.left
(2)mostright的右指针指向自己cur(因为第一步会指向自己),mostright.right = None,cur = cur.right向右走
(3)cur来到空的时候整个流程停
2.1 结构关系
- 经过例子得出:Morris序列是当树有左子树的会来到节点两次,只能两次递归自己,不能进行三次。来到节点一次,有左树回来一次,没有左树只来一次。
- morris序加工先、中、后序遍历时,没有左树,不用区分输出几次了,直接打印
2.2 代码
2.2.1 Morris序列(类似先后中序名称)(无打印)
# Morris序列
def morris(head):
if head is None:
return
cur = head # 最初指向头节点
mostRight = None # 人为操作修改的指针
while cur: # cur指向空就停
# 下面cur要么左动,要么右动
# cur如果没有左树,那么给mostright就是None,直接跳到最后一行代码了
mostRight = cur.left # 指向左子树的头节点 或 指向左孩子
if mostRight is not None: # cur.left 有左树
# 找到cur左树上,真实的最右节点
# 找最右节点,右指针为空(第一次来cur) and 右指针指向cur指向的节点(第二次来到cur)
while mostRight.right is not None and mostRight.right != cur:
mostRight = mostRight.right # 往右走
# 从while中出来,mostright一定是cur左树上的最右节点
#
if mostRight.right is None: # 第一次到来
mostRight.right = cur # 人为改动,如果右孩子空,就指向cur指向的节点
cur = cur.left
continue
else: # 一定等于cur,第二次来到cur
# mostRight.right != None -> mostRight.right == cur
mostRight.right = None # 让它重新指向空
cur = cur.right
2.2.2 Morris序列加工成先序序列
一个节点没有左树说明只会来到一次,直接打印
打印第一次遇到的点,就是先序遍历
# Morris序列 加工 先序遍历
def morrispre(head):
if head is None:
return
cur = head # 最初指向头节点
mostRight = None # 人为操作修改的指针
while cur: # cur指向空就停
# 下面cur要么左动,要么右动
# cur如果没有左树,那么给mostright就是None,直接跳到最后一行代码了
mostRight = cur.left # 指向左子树的头节点 或 指向左孩子
if mostRight is not None: # cur.left 有左树
# 找到cur左树上,真实的最右节点
# 找最右节点,右指针为空(第一次来cur) and 右指针指向cur指向的节点(第二次来到cur)
while mostRight.right is not None and mostRight.right != cur:
mostRight = mostRight.right # 往右走
# 从while中出来,mostright一定是cur左树上的最右节点
#
if mostRight.right is None: # 第一次到来
mostRight.right = cur
print(cur.value) # 第一次打印
cur = cur.left
continue
else: # 一定等于cur,第二次来到cur
# mostRight.right != None -> mostRight.right == cur
mostRight.right = None # 让它重新指向空
else: # 直接打印一次的节点
print(cur.value)
cur = cur.right
print()
2.2.3 Morris序列加工成中序序列
只遇到一次的节点打印,而遇到两次的节点只打印第二次出现的节点。
# Morris序列 加工 中序遍历
def morrisIn(head):
if head is None:
return
cur = head # 最初指向头节点
mostRight = None # 人为操作修改的指针
while cur: # cur指向空就停
# 下面cur要么左动,要么右动
# cur如果没有左树,那么给mostright就是None,直接跳到最后一行代码了
mostRight = cur.left # 指向左子树的头节点 或 指向左孩子
if mostRight is not None: # cur.left 有左树
# 找到cur左树上,真实的最右节点
# 找最右节点,右指针为空(第一次来cur) and 右指针指向cur指向的节点(第二次来到cur)
while mostRight.right is not None and mostRight.right != cur:
mostRight = mostRight.right # 往右走
# 从while中出来,mostright一定是cur左树上的最右节点
#
if mostRight.right is None: # 第一次到来
mostRight.right = cur # 人为改动,如果右孩子空,就指向cur指向的节点
cur = cur.left
continue
else: # 一定等于cur,第二次来到cur
# mostRight.right != None -> mostRight.right == cur
mostRight.right = None # 让它重新指向空
print(cur.value) # 往右移动就打印,回到两次会continue直接跳过 多加了打印
cur = cur.right
print()
2.2.4 Morris序列加工成后序序列
加工成后序遍历是有一些区别的。
第一次来到节点,逆序打印左树右边界,再打印整棵树的右边界
# Morris序列 加工 后序遍历
def morrisPos(head):
if head is None:
return
cur = head # 最初指向头节点
mostRight = None # 人为操作修改的指针
while cur: # cur指向空就停
# 下面cur要么左动,要么右动
# cur如果没有左树,那么给mostright就是None,直接跳到最后一行代码了
mostRight = cur.left # 指向左子树的头节点 或 指向左孩子
if mostRight is not None: # cur.left 有左树
# 找到cur左树上,真实的最右节点
# 找最右节点,右指针为空(第一次来cur) and 右指针指向cur指向的节点(第二次来到cur)
while mostRight.right is not None and mostRight.right != cur:
mostRight = mostRight.right # 往右走
# 从while中出来,mostright一定是cur左树上的最右节点
#
if mostRight.right is None: # 第一次到来
mostRight.right = cur
cur = cur.left
continue
else: # 能回到自己两次,且是第二次回到自己,逆序打印左树右边界
# mostRight.right != None -> mostRight.right == cur
mostRight.right = None # 让它重新指向空
printEdge(cur.left) # 打印子树的左边界
cur = cur.right
printEdge(head) # 整棵树的右边界也得逆序打印一下
print()
def printEdge(head):
tail = reverseEdge(head)
cur = tail
while cur:
print(cur.value)
cur = cur.right
reverseEdge(tail) # 再逆序回来,恢复原形
def reverseEdge(head): # 单链表反转再掉回来
pre = None
next = None
while head:
next = head.right
head.right = pre
pre = head
head = next
一棵树的分支,看作单链表,然后逆序
2.3 应用
2.3.1 判断二叉树是否为搜索二叉树
用Morris改动
# Morris序列 加工 中序遍历
def isBST(head):
if head is None:
return
cur = head # 最初指向头节点
mostRight = None # 人为操作修改的指针
pre = None
while cur: # cur指向空就停
# 下面cur要么左动,要么右动
# cur如果没有左树,那么给mostright就是None,直接跳到最后一行代码了
mostRight = cur.left # 指向左子树的头节点 或 指向左孩子
if mostRight is not None: # cur.left 有左树
# 找到cur左树上,真实的最右节点
# 找最右节点,右指针为空(第一次来cur) and 右指针指向cur指向的节点(第二次来到cur)
while mostRight.right is not None and mostRight.right != cur:
mostRight = mostRight.right # 往右走
# 从while中出来,mostright一定是cur左树上的最右节点
#
if mostRight.right is None: # 第一次到来
mostRight.right = cur # 人为改动,如果右孩子空,就指向cur指向的节点
cur = cur.left
continue
else: # 一定等于cur,第二次来到cur
# mostRight.right != None -> mostRight.right == cur
mostRight.right = None # 让它重新指向空
# print(cur.value) 把中序打印时机改成比对时机
if pre != None and pre >= cur.value: # 中序遍历必须是递增的
return False
pre = cur.value # 值为cur指向的值
cur = cur.right # cur移动
print()
2.3.2 返回二叉树最小高度
叶子节点距离头结点高度,才是树的高度
二叉树递归套路:
# 递归套路解二叉树高度
def minHeight1(head):
if head == None:
return 0
return p(head)
def p(x):
if x.left is None and x.right is None: # 左右树都为空+1
return 1
# 左右子树起码有一个不为空
leftH = float("inf")
if x.left:
leftH = p(x.left)
rightH = float("inf")
if x.right:
rightH = p(x.right)
return 1 + min(leftH,rightH)
Morris遍历套路:
问题:cur是否为叶节点,cur节点高度是多少?
不是叶节点不参与更新高度,最后看看整棵树的右边界即可。
# Morris序列 加工 二叉树高度
def minHeight2(head):
if head is None:
return
cur = head # 最初指向头节点
mostRight = None # 人为操作修改的指针
curLevel = 0
minHeight = float("inf")
while cur: # cur指向空就停
# 下面cur要么左动,要么右动
# cur如果没有左树,那么给mostright就是None,直接跳到最后一行代码了
mostRight = cur.left # 指向左子树的头节点 或 指向左孩子
if mostRight: # cur.left 有左树
# 找到cur左树上,真实的最右节点
# 找最右节点,右指针为空(第一次来cur) and 右指针指向cur指向的节点(第二次来到cur)
rightBoardSzie = 1 # 右边界
while mostRight.right and mostRight.right != cur:
rightBoardSzie += 1
mostRight = mostRight.right # 往右走
# 从while中出来,mostright一定是cur左树上的最右节点
if mostRight.right is None: # 第一次到来
curLevel += 1
mostRight.right = cur # 人为改动,如果右孩子空,就指向cur指向的节点
cur = cur.left
continue
else: # 一定等于cur,第二次来到cur
if mostRight.left is None:
minHeight = min(minHeight,curLevel) # 先把最小值高度抓出来
curLevel -= rightBoardSzie # 当前的值减去左树右边界节点个数就能更新我的层数
# mostRight.right != None -> mostRight.right == cur
mostRight.right = None # 让它重新指向空
else:
curLevel += 1 # 只能达到一次的节点 level++
finalRight = 1
cur = head
while cur.right:
finalRight += 1
cur = cur.right # cur移动
if cur.left and cur.right:
minHeight = min(minHeight,finalRight)
return minHeight
2.4 总结
如果需要左右树信息的话,就用二叉树递归套路
如果左右树的信息可以被代表,并不需要继续留着左右信息了,或用单独变量继承下去,用Morris遍历