1 条题解

  • 2
    @ 2023-4-24 19:17:26

    题意

    给定字符串 s(s106)s (|s| \le 10^6),找出它的最大回文长度。

    分析

    下面令 n=sn=|s|

    朴素

    枚举每一个子串,判断它是否为回文串即可,时间复杂度 O(n3)O(n^3)

    这里就不给出代码实现了

    哈希

    前置知识:字符串哈希 - OI Wiki (oi-wiki.org)

    首先预处理出字符串的哈希前缀和哈希后缀,方便快速 O(1)O(1) 比较两个子串是否相等。

    然后枚举回文串的对称轴,逐步向左右扩张,比较前后缀是否相等即可。

    时间复杂度 O(n2)O(n^2)

    但是注意到 s106|s| \le 10^6,所以这一定是不可过的,考虑优化。

    运用你的人类智慧可以发现,回文串的长度是具有单调性的。

    比如说 zroabcbaorz\texttt{zroabcbaorz},是一个回文串,那么 abcba\texttt{abcba} 也是一个回文串。

    这样不就可以直接枚举对称轴然后二分答案长度就可以了吗?

    代码:

    //the code is from chenjh
    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #define MAXN 1000005
    using namespace std;
    char s[MAXN];
    int n;
    const int p=103;//选择一个“较大的”且远离 2 的整数次幂的质数可以避免哈希冲突导致错误判断。
    unsigned long long p1[MAXN],p2[MAXN],pn[MAXN];//前缀哈希、后缀哈希、p 的 n 次幂。
    bool f(int pos,int x,bool op){
    	return p1[pos]-p1[pos-x]*pn[x]==p2[pos+op]-p2[pos+x+op]*pn[x];
    }//判断以 pos 为对称轴,半径为 x 的字符串是否为回文串。
    int main(){
    	pn[0]=1;
    	for(int i=1;i<MAXN;i++)pn[i]=pn[i-1]*p;//预处理 p 的 n 次幂。
    	for(int cnt=1;cin>>(s+1) && strcmp(s+1,"END");++cnt){
    		n=strlen(s+1);
    		for(int i=1;i<=n;i++) p1[i]=p1[i-1]*p+s[i];//使用字符串 BKDR 算法,预处理前缀哈希。
    		p2[n+1]=0;
    		for(int i=n;i;--i) p2[i]=p2[i+1]*p+s[i];//预处理后缀哈希值。
    		int ans=0;
    		for(int i=1;i<=n;i++){
    			int l,r;
    			if(i<n && s[i]==s[i+1]){//如果相邻两个字符相等,即回文串长度为偶数。
    				l=1,r=min(i,n-i);//进行二分长度。
    				while(l<r){
    					int mid=(l+r+1)>>1;
    					if(f(i,mid,1)) l=mid;
    					else r=mid-1;
    				}
    				ans=max(ans,l<<1);
    			}
    			l=1,r=min(i,n-i+1);//以第 i 个字符为对称轴,即回文串长度为奇数。
    			while(l<r){
    				int mid=(l+r+1)>>1;
    				if(f(i,mid,0)) l=mid;
    				else r=mid-1;
    			}
    			ans=max(ans,(l<<1)-1);
    		}
    		printf("Case %d: %d\n",cnt,ans);
    	}
    	return 0;
    }
    

    时间复杂度 O(nlogn)O(n \log n),十分优秀了!

    但是……这个怎么可能足够呢?!

    Manacher

    前置知识:Manacher - OI Wiki (oi-wiki.org)

    该算法由 Glenn K. Manacher 在 1975 年提出。

    其实这个问题可以使用后缀数组和快速 LCA 该问题可在 O(n)O(n) 时间内解决。

    但是这里描述的算法 压倒性 的简单,并且在时间和空间复杂度上具有更小的常数。

    鉴于 OI Wiki 介绍的十分详细,这里就不过多进行阐述。

    为了方便处理,我加入了 #\texttt{\#} 来表示空字符。

    在字符串开头我加入了 $\texttt{\$} 来防止数组越界。

    剩下的就十分简单了。

    代码:

    //the code is from chenjh
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #define MAXN 1000005
    char s[MAXN],t[MAXN<<1];
    int p[MAXN<<1];//定义 p[i] 为为字符 t[i] 为中心的最长回文子串的比南京。
    void manacher(int len){
    	int l=0;
    	t[l++]='$',t[l++]='#';//加入特殊字符 '$' 和 '#'。
    	for(int i=0;i<len;i++) t[l++]=s[i],t[l++]='#';
    	t[l]=0;
    	memset(p,0,sizeof p);//多测不清空,爆零两行泪!
    	for(int i=0,r=0,c=0;i<l;i++){
    		int &P=p[i];
    		P=r>i?std::min(p[(c<<1)-i],r-i):1;
    		for(;t[i+P]==t[i-P];++P);
    		if(i+P>r) r=i+P,c=i;
    	}
    }
    int main(){
    	for(int c=1;~scanf("%s",s)&&strcmp(s,"END");++c){
    		int len=strlen(s),ans=0;manacher(len);
    		for(int i=0,mi=(len+1)<<1;i<mi;i++) ans=std::max(ans,p[i]-1);//找到最大回文长度。
    		printf("Case %d: %d\n",c,ans);
    	}
    	return 0;
    }
    
    • 1

    信息

    ID
    2959
    时间
    15000ms
    内存
    64MiB
    难度
    10
    标签
    (无)
    递交数
    6
    已通过
    2
    上传者