POJ3974 Palindrome回文子串 hash+二分答案做法详解

                           POJ3974 Palindrome回文子串 hash+二分答案做法详解

Time Limit: 15000MS Memory Limit: 65536K
Total Submissions: 10983 Accepted: 4179

Description

Andy the smart computer science student was attending an algorithms class when the professor asked the students a simple question, "Can you propose an efficient algorithm to find the length of the largest palindrome in a string?" 

A string is said to be a palindrome if it reads the same both forwards and backwards, for example "madam" is a palindrome while "acm" is not. 

The students recognized that this is a classical problem but couldn't come up with a solution better than iterating over all substrings and checking whether they are palindrome or not, obviously this algorithm is not efficient at all, after a while Andy raised his hand and said "Okay, I've a better algorithm" and before he starts to explain his idea he stopped for a moment and then said "Well, I've an even better algorithm!". 

If you think you know Andy's final solution then prove it! Given a string of at most 1000000 characters find and print the length of the largest palindrome inside this string.

Input

Your program will be tested on at most 30 test cases, each test case is given as a string of at most 1000000 lowercase characters on a line by itself. The input is terminated by a line that starts with the string "END" (quotes for clarity). 

Output

For each test case in the input print the test case number and the length of the largest palindrome. 

Sample Input

abcbabcbabcba
abacacbaaaab
END

Sample Output

Case 1: 13
Case 2: 6

中文解释:在给定字符串中找最长回文子串

 

上图为hash+二分答案(算法时间复杂度O(N*log2N),未做常数优化常数较大,不过不影响)AC该题,本题时限是15000MS,测试4047MS,可见仍是非常快的。

 

在讲二分前,先说下本题的算法分析:

  1. 首先想到的就是模拟,纯模拟非常好想。用str[]数组来记录输入的字符,i枚举断点,左右方向进行拓展,每次打擂台比最大值。复杂度O(N2)。
  2. 模拟可以剪枝,甚至还可以换着各种方法模拟。先说最好想的剪枝,就是在左右方向拓展时,只要找到不一样的就break掉,数据强的话仍然是接近O(N2),只是我没试过能不能过。比较奇葩的方法就是用两个数组left[],right[]来表示断点左边和右边的字符,断点左移就是++left=right,right--;右移就是++right=left,left--;数组也可以换成vector,但都没什么用。
  3. 如果能想到用hash基本上就没问题了,如果裸hash实质还是暴力枚举,只是在拓展左右点时将字符比较转化成了数值比较,实际上复杂度仍没有变。
  4. 如果把hash和二分结合,那么就是这道题的O(N*log2N)解法(O(n)解法:Manacher)。

先来讲下二分答案:

二分答案是二分的一种,具体可以用一个小游戏说明:

对方心里想了一个整数,范围在1-1000,你现在要猜这个数字,对方会告诉你猜的数大了还是小了,如何才能最快的猜出来?

这个游戏正解为:无论数据是什么,我总能在log2n次内猜出来,上千万的值域内只需几十次就能猜对,这就是二分的效率。做法就是第一次我猜500,即(1000+1)/(整除)2,如果回答是小了,说明1-500都小了,那就从501-1000中取一半((501+1000)/2)再猜,反之从1-499中猜,一直猜到对。

这就是二分的思路,代码为:

1 while(l<r)
2 {
3     int mid=(l+r)/2;
4     if(a[mid]==x)return mid;
5     else if(a[mid]>x)l=mid;
6     else r=mid-1;
7 }

二分条件就是值域具有单调性,很明显这题答案一定具有单调性,而且区间也是确定的,意思就是一个长为55的字符串,它的回文子串最长也就55,所以答案一定在1,2,3,4,5···55之间,对这个范围二分即可,将上述代码的return的那一段修改一下即可。

每次二分取得的值为回文子串长,只需在字符中枚举断点,以O(1),hash的方式查找整个字符串中有没有这么大的子串,若有则找更大的有没有,若没有就往小里找,最少为1.提示:注意奇数偶数!!

思路就这么多,下面上代码:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef unsigned long long ull;
char str[1000010];
ull p[1000010];
ull h[1000010],f[1000010];
int ans[1000010];
int od[500010],am[500010];
int n;
inline void reset_p()//od[]存奇数,am[]存偶数
{
    p[0]=1;
    int amcnt=1,odcnt=1;
    for(int i=1;i<=1000000;i++)
    {
        p[i]=p[i-1]*131;
        if(i%2==0)am[amcnt++]=i;
        else od[odcnt++]=i;
    }
}
inline void inithas(int len)//h正序hash,f逆序hash
{
    for(int j=1;j<=len;j++)
        h[j]=h[j-1]*131+(str[j]-'a'+1);
    for(int j=1;j<=len;j++)
        f[j]=f[j-1]*131+(str[len-j+1]-'a'+1);
}

inline ull Find_l(int l,int r)
{
    return f[n-l+1]-f[n-r]*p[n-l+1-(n-r)];//找左边用逆序hash值
}
inline ull Find_r(int l,int r)
{
    return h[r]-h[l-1]*p[r-l+1];//右边正序
}
inline int _ask(int num)//进行判断整个字符串里有没有长度为num的回文串,分奇数和偶数进行判断,下标自己写个字符数一数就行了
{
    if(num%2==0)
    {
        int sbsth=num;
        num/=2;
        for(int i=num;i<=n-num;i++)
            if(Find_l(i-num+1,i)==Find_r(i+1,num+i))return sbsth;
        return 0;
    }
    else 
    {
        int sbsth=num;
        num/=2;
        for(int i=num;i<=n-num-1;i++)
            if(Find_l(i-num+1,i)==Find_r(i+2,i+num+1))return sbsth;
        return 0;
    }
}
inline int binary_Search_od(int left,int right)//二分答案中的奇数
{
    int maxx=1;
    while(left<=right)
    {
        int mid=(left+right)>>1;
        int val=_ask(od[mid]);
        if(val)
        {
            left=mid+1;
            maxx=max(maxx,val);
        }
        else 
            right=mid-1;
    }
    return maxx;
}
inline int binary_Search_am(int left,int right)//二分答案中的偶数
{
    int maxx=1;
    while(left<=right)
    {
        int mid=(left+right)>>1;
        int val=_ask(am[mid]);
        if(val)
        {
            left=mid+1;
            maxx=max(maxx,val);
        }
        else 
            right=mid-1;
    }
    return maxx;
}
int main()
{
    int cnt=0;
    reset_p();
    while(scanf("%s",str+1)&&(strcmp(str+1,"END")!=0))
    {
        cnt++;
        memset(h,0,sizeof(h));
        memset(f,0,sizeof(f));
        n=strlen(str+1);
        inithas(n);
        int ans1=binary_Search_od(1,n/2+1);
        int ans2=binary_Search_am(1,n/2+1);
        printf("Case %d: %d\n",cnt,max(ans1,ans2));
    }
    return 0;
}

这里说下二分两遍的原因:如果二分到的值为奇数,但字符串中不存在奇数回文串,这时右端点会往左移动,右边的数就不会考虑。问题就在这里,没有这么长的奇数长的回文串,不代表没有比这更长的偶数回文串,所以做法就是奇数个和偶数个都二分一遍。

转载于:https://www.cnblogs.com/zhw-c/p/8671993.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值