双倍回文(BZOJ-2342)(manacher+并查集)

2342: [Shoi2011]双倍回文

Time Limit: 10 Sec   Memory Limit: 128 MB
Submit: 3007   Solved: 1148
[ Submit][ Status][ Discuss]

Description

Input

输入分为两行,第一行为一个整数,表示字符串的长度,第二行有个连续的小写的英文字符,表示字符串的内容。

Output

输出文件只有一行,即:输入数据中字符串的最长双倍回文子串的长度,如果双倍回文子串不存在,则输出0

Sample Input

16
ggabaabaabaaball

Sample Output

12

HINT

N<=500000


题目链接:http://www.lydsy.com/JudgeOnline/problem.php?id=2342

题目大意:

输入数据中字符串的最长双倍回文子串的长度,如果双倍回文子串不存在,则输出0。

题目分析:

借鉴     ----------------->    http://blog.sina.com.cn/s/blog_8d5d2f04010196bh.html

因该算是一道挺不错的题,需要仔细思索一下

首先我可以看出:
(1)我们找到的串的本身也是一个回文串(显然)
(2)这个回文串的长度一定是偶数(显然)
(3)左右两个串一定也是偶数长度的回文串(显然)

那么我们先用manacher处理出以每个字符为中心的回文串长度
由于我们所需处理的这些串的长度都为偶数,所以这些串的中心都在manacher时的那些填充字符上(显然)

那么我们就先枚举大串的中心i,设左边小串的中心为j
那么j+rad[j]>=i    (rad[]为manacher中处理出的数组)
由于左边一定是回文串,那么rad[j]就应该要覆盖到i(不然怎么保证左边是回文串),而如果左边得到保证,那么右边也一定符合条件(对称)
所以我们就只需求出满足条件的最左侧的j

然后我们对j也有一个枚举范围,那就是在i的回文串范围内,并且还在i-rad[i]/2 ~ i 之间,不然不够

这样我们就可以初步得出一个枚举算法,那就是对于每个i,在一定范围内枚举j,找最优解
据说这个算法是可过的,但是复杂度。。。。似乎不是太乐观

于是需要优化
该优化其实也是显然的

如果我们曾枚举过一个j,它不能覆盖到当前枚举的i(也就是j+rad[j]
那么这个j,用一定不能覆盖到i+1(显然)
也就是说这个j在之后的计算中都没有用了,我们就不需要枚举了

这样我们就可以在枚举j的时候一段一段的跳,以降低复杂度
而实现这个过程,我们可以用并查集
每次都将没用的j的父亲指向j+1,然后跳到getfather(j+1)
这样就轻松完成了分段跳这个优化

最后在分析一下复杂度
(1)manacher   O(n)
(2)并查集      O(nα(n))
(3)每个点最多被删n次 O(n)
(4)每个点最多被利用一次 O(n)
(5)每个点最多被枚举一次 O(n)


上面是博客的内容

具体实现就是用以manacher算出的d数组进行计算,以每个i为中心,j在(i-d[i]/2,i)范围中移动计算。

代码:

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <queue>
#include <vector>
using namespace std;
typedef long long LL;
const int N=550000+999;
char s[N],pat[N*2];
int d[N*2],per[N*2];
void manacher(int len) //manacher模板
{
    int k,kmax;
    for(int i=1; i<=len ; i++)
    {
        pat[i*2]=s[i];
        pat[i*2+1]='#';
    }
    pat[0]='@';
    pat[1]='#';
    k=0;
    kmax=0;
    for(int i=2; i<=len*2; i++)
    {
        if(i<kmax) d[i]=min(d[2*k-i],kmax-i);
        else d[i]=1;
        while(pat[i+d[i]]==pat[i-d[i]]) d[i]++;
        if(i+d[i]>kmax)
        {
            kmax=i+d[i];
            k=i;
        }
    }
}
int f(int x) //并查集查询祖先
{
    if(per[x]==x)   return x;
    return   per[x]=f(per[x]);
}
int main()
{
    int n;
    scanf("%d",&n);
    scanf(" %s",s+1);
    memset(d,0,sizeof(d));
    memset(per,0,sizeof(per));
    manacher(n);
    //将所有字符串的#的祖先设为自己
    //非#设为下一个为#号的下标
    for(int i=1;i<=n*2;i++)
    {
        if(pat[i]=='#') per[i]=i;
        else per[i]=i+1;
    }
    int j,ans=0;
    for(int i=3; i<=n*2 ;i+=2)
    {
        //计算在以i为中心的回文串,左侧从哪开始枚举
        j=f(max(i-d[i]/2,1)); //j一定在以i为中心的回文串内且在(i-d[i]/2,i)中
        //j要在(i-d[i]/2,i)的范围内,如果构成双倍回文则存在 j+d[j]>=i
        while(j<i  &&  j+d[j]<i)
        {
            per[j]=f(j+1);//如果都不满足则,j继续在(i-d[i]/2,i)右移
            j=per[j];
        }
        //i-j的就是i左侧的回文串的长度,且j在i的左侧
        if(j<i && (i-j)*2 > ans)
            ans=(i-j)*2;
    }
    printf("%d\n",ans);
    return 0;
}

最近发现不用并查集也可以,想了一下发现并查集也只是实现了找到#的作用,可以直接判断就可以。

时间也从两百多降为一百多。

新代码:

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <queue>
#include <vector>
using namespace std;
typedef long long LL;
const int N=550000+999;
char s[N],pat[N*2];
int d[N*2];
void manacher(int len) //manacher模板
{
    int k,kmax;
    for(int i=1; i<=len ; i++)
    {
        pat[i*2]=s[i];
        pat[i*2+1]='#';
    }
    pat[0]='@';
    pat[1]='#';
    k=0;
    kmax=0;
    for(int i=2; i<=len*2; i++)
    {
        if(i<kmax) d[i]=min(d[2*k-i],kmax-i);
        else d[i]=1;
        while(pat[i+d[i]]==pat[i-d[i]]) d[i]++;
        if(i+d[i]>kmax)
        {
            kmax=i+d[i];
            k=i;
        }
    }
}
int main()
{
    int n;
    scanf("%d",&n);
    scanf(" %s",s+1);
    memset(d,0,sizeof(d));
    manacher(n);
    int j,ans=0;
    //枚举的中心一定是#,因为要枚举偶数串
    for(int i=3; i<=n*2 ;i+=2)
    {
        //计算在以i为中心的回文串,左侧从#开始枚举
        //j一定在以i为中心的回文串内且在(i-d[i]/2,i)中
        int x=i-d[i]/2;
        if(pat[x]=='#')
            j=x;
        else
            j=x+1;
        //j要在(i-d[i]/2,i)的范围内,如果构成双倍回文则存在 j+d[j]>=i
        while(j<i  &&  j+d[j]<i)
        {
            //如果都不满足则,j继续在(i-d[i]/2,i)右移
            j=j+2;
        }
        //i-j的就是i左侧的回文串的长度,且j在i的左侧
        if(j<i && (i-j)*2 > ans)
            ans=(i-j)*2;
    }
    printf("%d\n",ans);
    return 0;
}








  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值