题目链接
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;
}