POJ - 2217 Secretary 最长公共子串, 后缀数组+高度数组(或者哈希+二分)

题目链接

https://vjudge.net/problem/POJ-2217

题意:

t组数据,每组数据有两个字符串,判断这两个字符串的最长公共子串是多长。

题解

字符串的子串是必须连续的字符,子序列可以不连续,最长公共子序列可以用dp做。

最长公共子串常见的算法是后缀数组+高度数组。也可以二分+哈希去求。

据说还可以用后缀自动机O(n)的复杂度求,我太菜了,暂时还不会。

下面写一下两种算法,主要讲一下二分+哈希

后缀数组:

我看这个大佬的博客学的,图画的很棒https://www.cnblogs.com/shanchuan04/p/5324009.html

用的挑战书上的板子

倍增实现的后缀数组,复杂度O(n*logn*logn)

用基数排序的复杂度O(nlogn)

HDU - 1403  也是最长公共子串的题目,基数排序代码:https://paste.ubuntu.com/p/jjwXpTmbPV/

代码

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn=20010;
int ran[maxn],tem[maxn],sa[maxn],lcp[maxn];
int n,k;
bool compare_sa(int i,int j){
    if(ran[i]!=ran[j]) return ran[i]<ran[j];
    else{
        int x=i+k<=n?ran[i+k]:-1;
        int y=j+k<=n?ran[j+k]:-1;
        return x<y;
    }
}
void calc_sa(string s,int *sa){
    for(int i=0;i<=n;i++){
        sa[i]=i;
        ran[i]=i<n?s[i]:-1;
    }
    for(k=1;k<=n;k*=2){
        sort(sa,sa+n+1,compare_sa);
        tem[sa[0]]=0;
        for(int i=1;i<=n;i++){
            tem[sa[i]]=tem[sa[i-1]]+(compare_sa(sa[i-1],sa[i])?1:0);
        }
        for(int i=0;i<=n;i++){
            ran[i]=tem[i];
        }
    }
}
void calc_lcp(string s,int *sa,int *lcp){
    for(int i=0;i<=n;i++) ran[sa[i]]=i;
    int h=0;
    lcp[0]=0;
    for(int i=0;i<n;i++){
        int j=sa[ran[i]-1];
        if(h>0) h--;
        for(;j+h<n&&i+h<n;h++){
            if(s[j+h]!=s[i+h]) break;
        }
        lcp[ran[i]-1]=h;
    }
}
int main(){
    int t;
    cin>>t;
    getchar();
    while(t--){
        string s,ss;
        getline(cin,s);
        getline(cin,ss);
        int x=s.length();
        s=s+'$'+ss;
        n=s.length();
        calc_sa(s,sa);
        calc_lcp(s,sa,lcp);
        int ans=0;
        for(int i=1;i<n;i++){
            if(sa[i]<x!=sa[i+1]<x){
                ans=max(ans,lcp[i]);
            }
        }
        printf("Nejdelsi spolecny retezec ma delku %d.\n",ans);
    }
    return 0;
}

二分+哈希

设最长公共子串的长度为x,那么x在0到min(str1,str2)之间,并且满足二分的性质。因为如果存在长度为m的公共子串,那么必然存在长度小于m的公共子串。先哈希一下这两个字符串,二分长度。每次check的时候,假设判断是否存在长度为k的公共子串,那么看字符串1和字符串2长度为k的子串的哈希值有没有相同的,如果有返回true,没有返回false.

判断字符串1和字符串2是否存在长度为k的子串的哈希值相同,可以先将字符串1长度为k的哈希值放到set里,然后判断字符串2长度为k的哈希值在set里有没有,有返回true。因为用到了set,所以每次check复杂度nlogn,二分logn次,总的时间复杂度O(nlogn*logn),但是超时了。。。

然后想用STL里的unordered_set,但是poj不知道为啥不能用,一直编译错误,本地能过。unordered_set底层是哈希表,没有排序,增删改查都是O(1)的复杂度。

最后手写了个哈希表过了,用数组模拟邻接表处理哈希值冲突,AC了。没想到的是没处理哈希值冲突也能过。。。

      后来发现别人先把第一个串长度为k的哈希值放到数组里,然后排序,然后算出第二个串长度为k的哈希值,在数组里二分查找。这样过了。但是复杂度和用set的一样。可能STL卡常吧。。

下面给出各种代码:

用set超时代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<set>
#include<algorithm>
using namespace std;
const int maxn=10010;
typedef unsigned long long ull;
ull hs[maxn],hs2[maxn],f[maxn];
const int p=131;
char c[maxn],s[maxn];
int n,m;
ull get(int l,int r){
    return hs[r]-hs[l-1]*f[r-l+1];
}
ull get2(int l,int r){
    return hs2[r]-hs2[l-1]*f[r-l+1];
}
bool check(int x){
    set<ull> ss;
    for(int i=x;i<=n;i++){
        ss.insert(get(i-x+1,i));
    }
    for(int i=x;i<=m;i++){
        if(ss.find(get2(i-x+1,i))!=ss.end()) return true;
    }
    return false;
}
int main(){
    int t;
    cin>>t;
    getchar();
    f[0]=1;
    for(int i=1;i<maxn;i++) f[i]=f[i-1]*p;
    while(t--){
        gets(c+1);
        gets(s+1);
        n=strlen(c+1);
        m=strlen(s+1);
        for(int i=1;i<=n;i++) hs[i]=hs[i-1]*p+c[i];
        for(int i=1;i<=m;i++) hs2[i]=hs2[i-1]*p+s[i];
        int l=0,r=min(n,m)+1;
        while(l+1<r){
            int mid=l+r>>1;
            if(check(mid)) l=mid;
            else r=mid;
        }
        printf("Nejdelsi spolecny retezec ma delku %d.\n",l);
    }
    return 0;
}

用unordered_set编译错误,本地过的代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<unordered_set>
#include<algorithm>
using namespace std;
const int maxn=10010;
typedef unsigned long long ull;
ull hs[maxn],hs2[maxn],f[maxn];
const int p=131;
char c[maxn],s[maxn];
int n,m;
ull get(int l,int r){
    return hs[r]-hs[l-1]*f[r-l+1];
}
ull get2(int l,int r){
    return hs2[r]-hs2[l-1]*f[r-l+1];
}
bool check(int x){
    unordered_set<ull> ss;
    for(int i=x;i<=n;i++){
        ss.insert(get(i-x+1,i));
    }
    for(int i=x;i<=m;i++){
        if(ss.count(get2(i-x+1,i))) return true;
    }
    return false;
}
int main(){
    int t;
    cin>>t;
    getchar();
    f[0]=1;
    for(int i=1;i<maxn;i++) f[i]=f[i-1]*p;
    while(t--){
        gets(c+1);
        gets(s+1);
        n=strlen(c+1);
        m=strlen(s+1);
        for(int i=1;i<=n;i++) hs[i]=hs[i-1]*p+c[i];
        for(int i=1;i<=m;i++) hs2[i]=hs2[i-1]*p+s[i];
        int l=0,r=min(n,m)+1;
        while(l+1<r){
            int mid=l+r>>1;
            if(check(mid)) l=mid;
            else r=mid;
        }
        printf("Nejdelsi spolecny retezec ma delku %d.\n",l);
    }
    return 0;
}

手写哈希表,数组模拟邻接表处理哈希值冲突的代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<set>
#include<algorithm>
using namespace std;
const int maxn=10010;
typedef unsigned long long ull;
const ull mod=9973;
ull hs[maxn],hs2[maxn],f[maxn];
ull hstable[maxn];
int nex[maxn],head[maxn],tot=1;
const int p=131;
char c[maxn],s[maxn];
int n,m;
ull get(int l,int r){
    return hs[r]-hs[l-1]*f[r-l+1];
}
ull get2(int l,int r){
    return hs2[r]-hs2[l-1]*f[r-l+1];
}
bool check(int k){
    memset(head,-1,sizeof(head));
    tot=1;
    for(int i=k;i<=n;i++){
        ull x=get(i-k+1,i);
        int y=x%mod;
        hstable[tot]=x; nex[tot]=head[y];head[y]=tot;
        tot++;
    }
    bool flag=false;
    for(int i=k;i<=m;i++){
        ull x=get2(i-k+1,i);
        int y=x%mod;
        for(int j=head[y];j!=-1;j=nex[j]){
            if(hstable[j]==x){
                flag=true;
                break;
            }
        }
        if(flag) break;
    }
    return flag;
}
int main(){
    int t;
   scanf("%d",&t);
    getchar();
    f[0]=1;
    for(int i=1;i<maxn;i++) f[i]=f[i-1]*p;
    while(t--){
        gets(c+1);
        gets(s+1);
        n=strlen(c+1);
        m=strlen(s+1);
        for(int i=1;i<=n;i++) hs[i]=hs[i-1]*p+c[i];
        for(int i=1;i<=m;i++) hs2[i]=hs2[i-1]*p+s[i];
        int l=0,r=min(n,m)+1;
        while(l+1<r){
            int mid=l+r>>1;
            if(check(mid)) l=mid;
            else r=mid;
        }
        printf("Nejdelsi spolecny retezec ma delku %d.\n",l);
    }
    return 0;
}

没处理哈希值冲突也能过的代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<set>
#include<algorithm>
using namespace std;
const int maxn=10010;
typedef unsigned long long ull;
const ull mod=91746;
ull hs[maxn],hs2[maxn],f[maxn];
ull hstable[maxn*10];
const int p=131;
char c[maxn],s[maxn];
int n,m;
ull get(int l,int r){
    return hs[r]-hs[l-1]*f[r-l+1];
}
ull get2(int l,int r){
    return hs2[r]-hs2[l-1]*f[r-l+1];
}
bool check(int x){
    memset(hstable,0,sizeof hstable);
    for(int i=x;i<=n;i++){
        hstable[get(i-x+1,i)%mod]=get(i-x+1,i);
    }
    for(int i=x;i<=m;i++){
        if(hstable[get2(i-x+1,i)%mod]==get2(i-x+1,i)) return true;
    }
    return false;
}
int main(){
    int t;
   scanf("%d",&t);
    getchar();
    f[0]=1;
    for(int i=1;i<maxn;i++) f[i]=f[i-1]*p;
    while(t--){
        gets(c+1);
        gets(s+1);
        n=strlen(c+1);
        m=strlen(s+1);
        for(int i=1;i<=n;i++) hs[i]=hs[i-1]*p+c[i];
        for(int i=1;i<=m;i++) hs2[i]=hs2[i-1]*p+s[i];
        int l=0,r=min(n,m)+1;
        while(l+1<r){
            int mid=l+r>>1;
            if(check(mid)) l=mid;
            else r=mid;
        }
        printf("Nejdelsi spolecny retezec ma delku %d.\n",l);
    }
    return 0;
}

将第一个串的哈希值放到数组里排序,第二个串二分查找的代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=10010;
typedef unsigned long long ull;
ull hs[maxn],hs2[maxn],f[maxn];
const int p=131;
char c[maxn],s[maxn];
int n,m;
ull get(int l,int r){
    return hs[r]-hs[l-1]*f[r-l+1];
}
ull get2(int l,int r){
    return hs2[r]-hs2[l-1]*f[r-l+1];
}
bool check(int x){
    ull tem[maxn];
    int cnt=0;
    for(int i=x;i<=n;i++){
        tem[cnt++]=get(i-x+1,i);
    }
    sort(tem,tem+cnt);
    for(int i=x;i<=m;i++){
        ull y=get2(i-x+1,i);
        if(*lower_bound(tem,tem+cnt,y)==y) return true;
    }
    return false;
}
int main(){
    int t;
    cin>>t;
    getchar();
    f[0]=1;
    for(int i=1;i<maxn;i++) f[i]=f[i-1]*p;
    while(t--){
        gets(c+1);
        gets(s+1);
        n=strlen(c+1);
        m=strlen(s+1);
        for(int i=1;i<=n;i++) hs[i]=hs[i-1]*p+c[i];
        for(int i=1;i<=m;i++) hs2[i]=hs2[i-1]*p+s[i];
        int l=0,r=min(n,m)+1;
        while(l+1<r){
            int mid=l+r>>1;
            if(check(mid)) l=mid;
            else r=mid;
        }
        printf("Nejdelsi spolecny retezec ma delku %d.\n",l);
    }
    return 0;
}

我还写了一个哈希+二分实现的后缀数组,高度数组的代码,复杂度也是O(nlogn*logn)

暴力实现求后缀数组的复杂度是n*n*logn.因为快速排序复杂度是nlogn,字符串比较的复杂度是O(n).

哈希+二分可以将字符串的比较时间优化成O(logn),

二分找到两个字符串的公共前缀,然后比较公共前缀的后一个字符,这样字符串的比较时间就降到logn了。

acwing上有一道用二分+哈希+快排实现后缀数组,高度数组的题,题目链接:https://www.acwing.com/problem/content/142/

代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=20010;
typedef unsigned long long ull;
ull hs[maxn],f[maxn];
const int p=131;
int sa[maxn],height[maxn];
int n;
char c[maxn],s[maxn];
ull get(int l,int r){
    return hs[r]-hs[l-1]*f[r-l+1];
}
int max_common_prefix(int a,int b){
    int l=0,r=min(n-a+1,n-b+1)+1;
    while(l+1<r){
        int mid=l+r>>1;
        if(get(a,a+mid-1)==get(b,b+mid-1)) l=mid;
        else r=mid;
    }
    return l;
}
bool cmp(int a,int b){
    int len=max_common_prefix(a,b);
    int x=a+len>n?-1:c[a+len];
    int y=b+len>n?-1:c[b+len];
    return x<y;
}
int main(){
    int t;
    cin>>t;
    getchar();
    while(t--){
        gets(c+1);
        gets(s);
        int x=strlen(c+1);
        int m=strlen(s);
        c[x+1]='$';
        for(int i=0;i<m;i++) c[x+i+2]=s[i];
        n=x+m+1;
        c[n+1]=0;
        f[0]=1;
        for(int i=1;i<=n;i++){
            hs[i]=hs[i-1]*p+c[i]-'0';
            f[i]=f[i-1]*p;
            sa[i]=i;
        }
        sort(sa+1,sa+n+1,cmp);
        for(int i=1;i<=n;i++){
            if(i==1) height[i]=0;
            else height[i]=max_common_prefix(sa[i],sa[i-1]);
        }
        int ans=0;
        for(int i=1;i<n;i++){
            if(sa[i]<x!=sa[i+1]<x){
                ans=max(ans,height[i+1]);
            }
        }
        printf("Nejdelsi spolecny retezec ma delku %d.\n",ans);
    }
    return 0;
}

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值