LeetCode 1044. 最长重复子串--二分查找+Rabin-Karp 字符串编码+Hash查找

  1. 最长重复子串
    给出一个字符串 S,考虑其所有重复子串(S 的连续子串,出现两次或多次,可能会有重叠)。

返回任何具有最长可能长度的重复子串。(如果 S 不含重复子串,那么答案为 “”。)

示例 1:

输入:“banana”
输出:“ana”
示例 2:

输入:“abcd”
输出:""

提示:

2 <= S.length <= 10^5
S 由小写英文字母组成。

题解

很有趣的题目,首先二分查找可以知道,就是二分长度,如果长度为mid的字符串出现了至少两次,那么就可能为答案并继续二分。确定是二分查找后,我们开始思考最麻烦的问题,怎么判断一个长度为mid的字符串出现了至少两次,换句话说就是长度至少为mid的字符串出现了多次,怎么找呢?这是最麻烦的地方,直接暴力判断肯定超时,不用想,我甚至一开始利用快速排序的排序过程进行比较判断,还是超时,因为字符串长度为n,找的子字符串长度为mid,那么需要比较判断的时间复杂度差不多为m x n,我就在想如果是数字该多好,相当于就是n个数字找是否有重复的就行,那么就很方便了,直接一个数组标记这个数字是否之前出现过,就非常简单,可惜这个是字符串,于是我又突然想到,对字符串进行编码!编码成数字,不就很简单了吗,也就是Rabin-Karp 字符串编码,不知道这个编码方式的可以百度了解下,不过理解起来也非常简单,如果我给你一个二进制数:01010转换成10进制数就是

=0 x 2 ^ 4 + 1 x 2 ^ 3 + 0 x 2 ^ 2 + 1 x 2 ^ 1 + 0 x 2 ^ 0

因为这是二进制数,那么同样的同理呀,字符串题目规定了只有26个小写字母构成,不就可以看成以26为基底的26进制数了吗?那么剩下的转换就很简单了吧(数学真奇妙)。

好了,我们解决了字符串转数字的问题,又有新的问题,因为字符串最长为1e5,需要构成的子字符串长度可以到达1e5-1,这样长的子字符串计算得到的数字是非常大的,于是我们需要利用Hash查找,对一个数字取模,并用pos[maxn]数组标记每个字符串转换的数字取模的结果,标记字符串的位置,比如pos[ans]=0就表示以0为开头的字符串转换为数字取模后为ans,那么同样的其他字符串也这样转换,同样的,再往后遍历,如果出现同样的ans,需要进行特判,因为存在取模相同但是表示的字符串不同的情况,然后用碰撞检测方法判断就行。

AC代码

class Solution {
public:
    int mod=1000000;
    int pos[1001000];//每个编码对应的字符串位置(表示这个字符串)
    bool is_equal(int s1,int s2,int n,string s)
    {
        for(int i=s1,j=s2;i<=s1+n-1;i++,j++)
        if(s[i]!=s[j])return false;
        return true;
    }
    string check(string s,int n)
    {
        memset(pos,-1,sizeof(pos));
        int ans=0,base=1;
        for(int i=0;i<n;i++)
        {
            if(i<n-1)
            {
                base*=26;
                base%=mod;
            }
            ans*=26;
            ans+=(s[i]-'a');
            ans%=mod;
        }
        pos[ans]=0;//以下标0开始构成的字符串
        for(int i=n;i<s.length();i++)
        {
            ans=(ans-((s[i-n]-'a')*base%mod)+mod)%mod;
            ans*=26;
            ans+=(s[i]-'a');
            ans%=mod;
            if(pos[ans]==-1)
            {
                pos[ans]=i-n+1;//字符串开始的下标
            }
            else//进行特判,避免出现编码相同字符串不同的情况
            {
                if(is_equal(i-n+1,pos[ans],n,s))//如果字符串相同
                {
                    string t="";
                    for(int j=i-n+1;j<=i;j++)
                    t+=s[j];
                    return t;
                }
                else//出现碰撞,也就是编码相同但是字符串不同
                {
                    for(int j=1;j<=1000;j++)//平方指针探测法
                    {
                        int new_ans=(ans+j*j)%mod;
                        if(pos[new_ans]==-1)//找到了没有被占用的编码
                        {
                            pos[new_ans]=i-n+1;
                            break;
                        }
                    }
                }
            }
        }
        return "";
    }
    string longestDupSubstring(string s) 
    {
        int l=1,r=s.length()-1;
        string fin="";
        while(l<=r)
        {
            int mid=(l+r)/2;
            string res=check(s,mid);
            if(res!="")
            {
                fin=res;
                l=mid+1;
            }
            else r=mid-1;
        }
        return fin;
    }
};

LeetCode 1044. 最长重复子串--二分查找+Rabin-Karp 字符串编码+Hash查找_第1张图片

你可能感兴趣的:(LeetCode,二分查找,字符串,算法,数据结构,leetcode)