HJ 32&85 最长回文子串【python3】

回文串

所谓回文串,指左右对称的字符串。
所谓子串,指一个字符串删掉其部分前缀和后缀(也可以不删)的字符串

题目描述

Catcher是MCA国的情报员,他工作时发现敌国会用一些对称的密码进行通信,比如像这些ABBA,ABA,A,123321,但是他们有时会在开始或结束时加入一些无关的字符以防止别国破解。比如进行下列变化 ABBA->12ABBA,ABA->ABAKK,123321->51233214 。因为截获的串太长了,而且存在多种可能的情况(abaaab可看作是aba,或baaab的加密形式),Cathcer的工作量实在是太大了,他只能向电脑高手求助,你能帮Catcher找出最长的有效密码串吗?

数据范围:字符串长度满足 1 ≤ n ≤ 2500 1 \le n \le 2500 1n2500

可见题目本意就是求一个字符串的最大回文子串的长度,因此HJ32和HJ85放在一起讲

输入描述

输入一个字符串(字符串的长度不超过2500)

输出描述

返回有效密码串的最大长度

代码&解释

穷举 HJ85

一个解决方案就是穷举,将字符串所有的子串列出,判断是否为回文子串,最坏输出最长回文子串。但是此穷举法也未必要把所有子串列出,这只是最坏情况。
由于我们所求是最长回文子串,因此对于列举出的子串,可以从最大长度即输入的字符串本身开始寻找回文子串。子串的长度递减,找到后立即退出循环输出,得到就是最长回文子串的长度。
其中,由于回文串是中心对称的,故回文串倒序输出和正序输出应当是相同结果,以此作为是否为回文串的判断

"""
找最大回文子串 HJ85
"""
def max_huiwen(s):
    for length in range(len(s),-1,-1): #长度递减
        for index in range(0,len(s)-length+1):
            sub_string = s[index:length+index]
            if sub_string == sub_string[::-1]:
                    #由于对称性,回文的倒序和正序相同
                return length

S=list(input())
print(max_huiwen(S))

穷举法在HJ85可通过全部用例,在HJ32超时,因此在做HJ32的时候采用了下面动态规划的方法

动态规划1-二维数组

以二维数组dp[i][j]记录字符串s[i]~s[j]是否为回文子串,若是则值为子串长度,若不是则值为0。
变量len_max比较记录最长回文子串的长度,以便在补完dp数据的同时记录下长度。

dp=[[1]*n for _ in range(n)]
len_max=0

先对数组做初始化,显然任意单个字符都是回文子串;而任意长度为2的子串,当且仅当两个字符相同为回文子串。

for i in range(n):
    dp[i][i]=1
    len_max=1
for i in range(n-1):
    if S[i]==S[i+1]:
        dp[i][i+1]=2
        len_max=2

之后分析动态数量关系,补充dp数组的数据。
若要使s[i] ~ s[j]为回文串,则需要满足:s[k]=s[j+i-k]( i ≤ k ≤ j i\le k \le j ikj)。即s[k]=s[j+i-k]( i + 1 ≤ k ≤ j − 1 i+1\le k \le j-1 i+1kj1),s[i+1] ~ [j-1]为回文串且s[i]与s[j]是相同字符。故dp[i][j]只需与dp[i+1][j-1]构建关系。
同时记录回文子串的长度,与len_max进行比较,最后得出的结果即为最长回文子串。
而由于dp[i][j]的数据和下一行上一列的数据关联且长度不超过2的dp已经处理,故i从n-2开始递减,j从i+2递增的形式循环

for i in range(n-3,-1,-1):
    for j in range(i+2,n,1):
        if dp[i+1][j-1]>0 and S[i]==S[j]:
            dp[i][j]=j-i+1
            if j-i+1>len_max:
                len_max=j-i+1

下面是全部代码

S=input()
n=len(S)
# dp[i][j]表示S[i]~S[j]是否为回文串
# 不是返回0 否则返回长度
dp=[[0]*n for _ in range(n)]
len_max=0

# 初始化:单个字符是回文&长度为2的回文判断
for i in range(n):
    dp[i][i]=1
    len_max=1
for i in range(n-1):
    if S[i]==S[i+1]:
        dp[i][i+1]=2
        len_max=2

# 当且仅当dp[i+1][j-1]回文时,dp[i][j]才有可能回文
# 从中心开始向外扩散
for i in range(n-3,-1,-1):
    for j in range(i+2,n,1):
        if dp[i+1][j-1]>0 and S[i]==S[j]:
            dp[i][j]=j-i+1
            if j-i+1>len_max:
                len_max=j-i+1


print(len_max)

动态规划2-一维数组

动态规划1的代码在牛客通过全部用例要668ms。复习的时候想只用一维数组dp实现动态规划,看看能不能实现优化。由于本人不怎么会算复杂度,单从运行时间上来看是失败了。不过牺牲了时间复杂度换取了空间复杂度,占用的内存是原来的 1 10 \frac1{10} 101
因此也附上思路和代码。
由动态规划1的思路可知,dp[i][j]只和dp[i+1][j-1]的数值有关,即dp数组当前一行的数据只与下一行的数据有关,因此dp数组只需要存储下一行的数据即可。
为了方便讨论,我们将原来的二维dp数组记为arr。

  1. 我们这次设len_max的初值为1,那么arr[n-1][]行我们可以不用管了
  2. 索引i仍是递减处理,只不过这次从i=n-2开始递减。j与动态规划1不同,则是从n开始递减至i+1。
    这样处理是因为在处理arr[i][j]时,我们需要arr[i+1][j-1]的数据,由于我们是一边查看历史的dp数值的同时存储当前dp值的,因此在列上需要从右往左处理。这样更新当前dp[j]时,dp[j-1]存储的仍然是下一行的数据,而没有被本行的dp[j-1]值覆盖掉
  3. 特别的,当我们处理arr[i][i+1]时,需要arr[i+1][i]的值,dp数组是没有记录到这个历史数据的,因此需要添加条件处理子串长度为2的情况。而我们处理arr[i][i]时是不需要用到历史数据,而且对更新当前行数据也没有任何影响的,可以在外层循环开始的时候更新。
  4. 最后要注意,和动态规划1不同,不是回文子串的部分,dp数值要记得更新为0。
S=input()
n=len(S)

dp=[0]*n
len_max=1

# i从n-2开始递减
for i in range(n-2,-1,-1):
    dp[i]=1
    for j in reversed(range(i+1,n,1)):
        if j>i+1 and dp[j-1]>0 and S[i]==S[j]:
            dp[j]=j-i+1
            if j-i+1>len_max:
                len_max=j-i+1
        elif j==i+1 and S[i]==S[j]:
            dp[j]=2
        else:
            dp[j]=0

print(len_max)

优化-Manacher算法

记原字符串为S,处理后的字符串为T,一维数组length[i]记录以T[i]为中心的最长回文子串的最右边一个字符到T[i]组成的字符串的长度。例如,T[j]是以T[i]为中心的最长回文子串的最右边一个字符,则length[i]=j-i+1

在每个字符之间和字符串S的首尾插入一个分割符

分隔符只能是字符串S中没有的字符

length数组的性质和计算
性质
  1. 对于任何一个S内长度为m的回文子串,需要插入m+1个分隔符,故这个回文子串在T内的长度为2m+1,一定是奇数。故对于以T[i]为中心的最长回文子串而言,有length[i]个分隔符,有length[i]-1个在原字符串内的非分割字符。
    那么我们求S最大回文子串的长度,等价于求数组len的最大值M,则M-1即为所求。
  2. 由于单个字符一定是回文子串,显然length[0]=1,length[1]=2。
计算

我们从左往右依次计算length[i],则此时length[k]( 0 ≤ k < i 0\le k<i 0k<i)是已知的。
记right为以k( 0 ≤ k < i 0\le k<i 0k<i)为中心的最长回文子串的右端点的最大位置;center记录right所在最大回文子串,其中心点的位置。

  1. i ≤ r i g h t i\leq right iright
    记j为i关于center的对称点。
    (1). 若length[j]<right-i:
    由length数组的性质与i,j的位置对称性可知,以j为中心的最长回文子串一定在以center为中心的最长回文子串的内部。并且由回文串的对称性和i,j的位置对称性可知,以i为中心的最长回文子串也在以center为中心的最长回文子串的内部,且二者相同。故length[i]=length[j]
    (2). 若length[j] ≥ \geq right-i:
    则以i,j为中心的最长回文子串可能在以center为中心的最长回文子串的外部。故从位置right+1开始与对称的位置2i-right-1匹配,直到无法匹配。设最后右端匹配到位置k,则length[i]=k-i+1,并且更新right和center

  2. i > r i g h t i> right i>right
    则以i为中心的最大回文子串一定有一部分在以center为中心的最大回文子串的外部。故从位置i+1开始匹配,直到无法匹配。同理得到length[i]以及更新right和center

Manacher算法求最大回文子串长度代码

S=list(input())
max_len=1

# 对S的每个字符以及头尾之间插入字符#,得到新的字符串T
for i in range(len(S)):
    tmp='#'+S[i]
    S[i]=tmp
S.append('#')
T=''.join(S)


# 初始化数组length以及right,center
length=[0]*len(T)
length[0],length[1]=1,2


right,center=2,1

# Manacher算法
S=list(input())
max_len=1

# 对S的每个字符以及头尾之间插入字符#,得到新的字符串T
for i in range(len(S)):
    tmp='#'+S[i]
    S[i]=tmp
S.append('#')
T=''.join(S)


# 初始化数组length以及right,center
length=[0]*len(T)
length[0],length[1]=1,2


right,center=2,1

# Manacher算法
for i in range(2,len(T)):
    #print('i=',i,'right=',right,'center=',center)

    # i在以center为中心的最长回文子串内部
    if i<=right:
        j=2*center-i # i关于center的对称位置
        if length[j]<right-i:
            length[i]=length[j]
        else:
            flag=1
            for k in range(right+1,len(T)):
                if T[k]!=T[2*i-k]:
                    length[i]=k-i
                    flag=0
                    break
            # 知道字符串结尾都匹配成功的话,做特殊处理
            if flag==1:
                length[i]=len(T)-i
            
    
    else: # i在以center为中心的最长回文子串外部
        flag=1
        for k in range(i+1,len(T)):
            if T[k]!=T[2*i-k]:
                length[i]=k-i
                flag=0
                break
        if flag==1:
            length[i]=len(T)-i
            

    # 更新right和center的值
    tmp=i+length[i]-1
    if tmp>right:
        right=tmp
        center=i



    if length[i]>max_len:
        max_len=length[i]

    #print('len[i]=',length[i])

print(max_len-1)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值