算法训练 Tricky and Clever Password(Python)

问题描述

在年轻的时候,我们故事中的英雄——国王 Copa——他的私人数据并不是完全安全地隐蔽。对他来说是,这不可接受的。因此,他发明了一种密码,好记又难以破解。后来,他才知道这种密码是一个长度为奇数的回文串。

Copa 害怕忘记密码,所以他决定把密码写在一张纸上。他发现这样保存密码不安全,于是他决定按下述方法加密密码:他选定一个整数 X ,保证 X 不小于 0 ,且 2X 严格小于串长度。然后他把密码分成 3 段,最前面的 X 个字符为一段,最后面的 X 个字符为一段,剩余的字符为一段。不妨把这三段依次称之为 prefix, suffix, middle 。显然, middle 的长度为一个大于 0 的奇数,且 prefix 、 suffix 的长度相等。他加密后的密码即为 A + prefix + B + middle + C + suffix ,其中 A 、 B 、 C 是三个由 Copa 选定的字符串,且都有可能为空, + 表示字符串相连。

许多年过去了。Copa 昨天找到了当年写下加密后字符串的那张纸。但是,Copa 把原密码、A、B、C 都忘了。现在,他请你找一个尽量长的密码,使得这个密码有可能被当年的 Copa 发明、加密并写下。

解决方案

这个问题模型抽象出来就是给定一个字符串str,str=A+pre+B+middle+C+suf,其中A,B,C可以为空,pre和suf也可以为空。题目想让我们给出pre+middle+suf最大情况下,这三部分的长度。middle是长度为奇数的回文串,pre+suf也可以组成回文串。

我们知道,pre和suf的长度是相等的,并且suf一定在给定的字符串末尾,假设现在middle长度是一个常数,我们自然想找到最大的X(X指代pre或suf的长度),我们当然可以枚举suf的长度,然后在字符串前段进行匹配进而得到最大的X,但是有更为简洁的方法。
我们通过计算可以得到f数组,f[i]表示在给定的str中,以i结尾的前缀和(不是“前缀和”而是“前缀”“和”)str翻转后的最大匹配长度,举个例子:

str=abhiba
str_reverse=abihba
那我们可以看到f[1]=a;f[2]=2;f[3]=2…(下标从1开始)

f数组利用KMP可以直接求得,不了解的朋友可以先去看下kmp算法,网上有很多好的博客。
再引进一个dp数组
dp[i]=max(dp[i-1],f[i])
如何理解呢? dp[i]表示的就是前i个字符中 和 后缀的最大匹配长度(这里我叙述的不是很严谨,我想表达的是在前i个字符中找到最长的符合题意pre)

现在对于pre和suf我们已经解决了,再来看刚才遗留的问题,我们假定middle是个常数,但是middle的长度我们该怎么得到呢?
这一部分就需要用到Manacher算法了,预处理得到str每一个位置的回文半径,这样的话,我们枚举每一个回文半径,找到pre的最大长度,最后相加取max即可。
细节部分大家看下代码吧,我感觉写的还是算清晰的,我也是初学,大家见谅。

def KMP(s,p):
    s=' '+s
    p=' '+p
    n=len(s)-1
    m=len(p)-1
    match=[None for i in range(n+1)]
    ne=[0 for i in range(m+10)]
    j=0
    for i in range(2,m+1):
        while(j and p[i]!=p[j+1]): j=ne[j]
        if p[i]==p[j+1]: j+=1
        ne[i]=j
    j=0
    for i in range(1,n+1):
        while(j and s[i]!=p[j+1]): j=ne[j]
        if s[i]==p[j+1]: j+=1
        match[i]=j
    return match
def init(st):
    tmp=[None for i in range((len(st)<<1)+3)]
    str_len=len(st)
    tmp[0]='@'
    tmp[1]='#'
    for i in range(str_len):
        tmp[(i+1)*2]=st[i]
        tmp[(i+1)*2+1]='#'
    tmp[2*str_len+2]='&'
    return tmp
def manacher(st):
    s_len=(len(st))
    p=[None for i in range(s_len)]
    C=0;R=0;ans=0
    for i in range(1,s_len-1):
        i_mirror=2*C-i
        if R>i:
            p[i]=min(R-i,p[i_mirror])
        else:
            p[i]=0
        while(st[i+1+p[i]]==st[i-1-p[i]]):
            p[i]+=1
        if i+p[i]>R:
            C=i
            R=i+p[i]
    return p
s=input()
dp=[0 for i in range(len(s)+10)]
tail=[0 for i in range(len(s)+10)]
p=manacher(init(s))
re_s=list(s);re_s.reverse()
re_s=''.join(re_s)
f=KMP(s,re_s)
for i in range(1,len(s)+1):
    if dp[i-1]<f[i]:
        dp[i]=f[i];tail[i]=i
    else:
        dp[i]=dp[i-1];tail[i]=tail[i-1]
ans=0
for i in range(2,len(s)+1):
    center=2*i
    R=p[center]//2+1
    pre_suf=min(dp[i-R],len(s)-(i+R-1))
    if 2*pre_suf+2*R-1>ans:
        final=[]
        ans=2*pre_suf+p[center]
        if pre_suf: final.append((tail[i-R]-pre_suf+1,pre_suf))
        final.append((i-R+1,p[center]))
        if pre_suf: final.append((len(s)-pre_suf+1,pre_suf))
print(len(final))
for i in final:
    print(i[0],i[1])

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值