写作原因:
我看网上大部分都是二分哈希的做法但却没有后缀自动机SAM的解法代码,因此提供两个SAM的解法,两个解法我都不详细介绍,会提供原文章地址和我个人的代码。个人认为第二种解法在时间和空间复杂度上都更优,也更好理解(前提你熟悉两个字符串情况下的SAM解法)。
SAM解法的优点:
时间复杂度是O(N)的,在数据量大的时候会快很多,可以通过数据更强的SPOJ.com - Problem LCS2
题意:
输入k个字符串,求这k个字符串间的最长公共子串的长度。
解法一:
原地址:后缀自动机 (SAM) - OI Wiki (oi-wiki.org)
我的AC代码:
//
// Created by 15411 on 2024/8/30.
//
#include<bits/stdc++.h>
#define ll long long
#define int long long
#define endl "\n"
using namespace std;
const int N=2e5+7;
int k,ans;//字符串数量
string s,t;
namespace SAM{
struct node{
int link,len;
int son[27],vst[10];//记录十种&的可能
}t[N<<1];
int sz,last,kk;
bitset<N<<1>vst;
void newNode(int length){
t[++sz].len=length;
t[sz].link=-1;
memset(t[sz].son,0,sizeof(t[sz].son));
memset(t[sz].vst,0,sizeof(t[sz].vst));
}
void init(){
for(int i=0;i<=sz;i++)vst[i]=false;
sz=-1;last=0;ans=0;kk=-1;
newNode(0);
}
void insert(int c){
newNode(t[last].len+1);
if(c<0){
vst[sz]=true;
t[sz].vst[++kk]=true;
c=26;//&
}
int p=last,cur=sz;
while(p!=-1&&!t[p].son[c])
t[p].son[c]=cur,p=t[p].link;
if(p==-1)
t[cur].link=0;
else{
int q=t[p].son[c];
if(t[q].len==t[p].len+1)
t[cur].link=q;
else{
newNode(t[p].len+1);
int nq=sz;
memcpy(t[nq].son,t[q].son,sizeof(t[q].son));
memcpy(t[nq].vst,t[q].vst,sizeof(t[q].vst));
if(vst[q])vst[nq]=true;
t[nq].link=t[q].link;
t[cur].link=t[q].link=nq;
while(p!=-1&&t[p].son[c]==q)
t[p].son[c]=nq,p=t[p].link;
}
}
last=cur;
}
vector<int>G[N<<1];
void dfs(int now){
for(auto next:G[now]){
dfs(next);
for(int i=0;i<k;i++){
if(t[next].vst[i])
t[now].vst[i]=true;
}
}
}
void Find(int now){//查询当前点的子中有几个&
vst[now]=true;
for(int i=0;i<27;i++){
if(!t[now].son[i])continue;
int cd=t[now].son[i];
if(!vst[cd])Find(cd);
for(int j=0;j<k;j++) {
if(t[cd].vst[j])
t[now].vst[j]=true;
}
}
int cnt=0;
for(int i=0;i<k;i++)if(t[now].vst[i])cnt++;
if(cnt==k)
ans=max(ans,t[now].len);
}
void query(){
for(int i=0;i<=sz;i++)G[i].clear();
for(int i=1;i<=sz;i++)G[t[i].link].push_back(i);
dfs(0);
Find(0);
}
}
void solve() {
cin>>k;
s.clear();
for(int i=1;i<=k;i++){
cin>>t;
s+=t;
s+='&';
}
int len=(int)s.size();
SAM::init();
for(int i=0;i<len;i++){
SAM::insert(s[i]-'a');
}
SAM::query();
cout<<ans<<endl;
}
signed main(){
ios::sync_with_stdio(false);//cin.tie(0);cout.tie(0);
solve();
return 0;
}
解法二
原地址:Longest Common Substring II SPOJ - LCS2 (后缀自动机) - Jiaaaaaaaqi - 博客园 (cnblogs.com)
我的AC代码:
//
// Created by 15411 on 2024/8/30.
//
#include<bits/stdc++.h>
#define ll long long
#define int long long
#define endl "\n"
using namespace std;
const int N=2e5+20;
const int INF=1e9+7;
int k,LCM;//字符串数量
string s;
char t[N];
namespace SAM{
struct node{
int link,len;
int cnt,lcm_last,lcm_now;//之前串的每个点的LCM取min,当前串每个点的LCM取max
int son[26];
}t[N<<1];
int sz,last;
void newNode(int length){
t[++sz].len=length;
t[sz].link=-1;t[sz].cnt=1;t[sz].lcm_last=INF;
memset(t[sz].son,0,sizeof(t[sz].son));
}
void init(){
sz=-1;last=0;
newNode(0);
}
void insert(int c){
newNode(t[last].len+1);
int p=last,cur=sz;
while(p!=-1&&!t[p].son[c])
t[p].son[c]=cur,p=t[p].link;
if(p==-1)
t[cur].link=0;
else{
int q=t[p].son[c];
if(t[q].len==t[p].len+1)
t[cur].link=q;
else{
newNode(t[p].len+1);
int nq=sz;
memcpy(t[nq].son,t[q].son,sizeof(t[q].son));
t[nq].link=t[q].link;
t[cur].link=t[q].link=nq;
while(p!=-1&&t[p].son[c]==q)
t[p].son[c]=nq,p=t[p].link;
}
}
last=cur;
}
int ans[N];
vector<int>G[N<<1];
void dfs(int now){
for(auto next:G[now]){
dfs(next);
t[now].lcm_now=max(t[now].lcm_now,t[next].lcm_now);
t[now].lcm_now=min(t[now].len,t[now].lcm_now);//父节点虽然匹配上了,但要防止越界
}
}
void init2(){
for(int i=0;i<sz;i++)G[i].clear();
for(int i=1;i<=sz;i++)G[t[i].link].push_back(i);
}
void query(char* s) {
for (int i = 0; i <= sz; i++)t[i].lcm_now = 0;
int now = 0, n = (int) strlen(s);
for (int i = 0; i < n; i++) {
if (t[now].son[s[i] - 'a']) {
now = t[now].son[s[i] - 'a'];
ans[i + 1] = ans[i] + 1;
} else {
while (now != -1 && !t[now].son[s[i] - 'a'])
now = t[now].link;
if (now == -1) {
now = 0;
ans[i + 1] = 0;
} else {
ans[i + 1] = t[now].len + 1;
now = t[now].son[s[i] - 'a'];
}
}
t[now].lcm_now = max(t[now].lcm_now, ans[i + 1]);
}
dfs(0);
for (int i = 0; i <= sz; i++) {
if (!t[i].lcm_now)continue;
t[i].cnt++;//记得统计这个点是不是被匹配了k次
t[i].lcm_last = min(t[i].lcm_last, t[i].lcm_now);
}
}
void query(){
LCM=0;
for(int i=0;i<=sz;i++){
if(t[i].cnt==k)
LCM=max(LCM,t[i].lcm_last);
}
}
}
void solve() {
cin>>k>>s;
int len=(int)s.size();
SAM::init();
for(int i=0;i<len;i++)SAM::insert(s[i]-'a');
SAM::init2();
for(int i=2;i<=k;i++){
cin>>t;
SAM::query(t);
}
SAM::query();
if(k==1)
LCM=len;
cout<<LCM;
}
signed main(){
ios::sync_with_stdio(false);//cin.tie(0);cout.tie(0);
solve();
return 0;
}
思路3:
二分长度+哈希验证。
我的二分哈希代码:
//
// Created by 15411 on 2024/8/31.
//
#include<bits/stdc++.h>
#define ll long long
#define int long long
#define endl "\n"
using namespace std;
const int N=2e4+7;
namespace Hash{
const int base1=13331,MOD1=1e9+7;
int P1[N],H1[N];
void init(char* s,int len){
P1[0]=1;H1[0]=s[0]-'a';
for(int i=1;i<len;i++){
H1[i]=(H1[i-1]*base1)%MOD1+(s[i]-'a');H1[i]%=MOD1;
P1[i]=(P1[i-1]*base1)%MOD1;
}
}
int get_hash(int L,int R){
int ret=H1[R]-H1[L-1]*P1[R-L+1];
ret=(ret%MOD1+MOD1)%MOD1;
return ret;
}
}
char s[11][N];
int k;
bool yz(int LEN){
map<ll,bool>vst[2];
ll len=(ll)strlen(s[1]);
Hash::init(s[1],len);
for(int L=0,R=L+LEN-1;R<len;R++,L++){
int H_now=Hash::get_hash(L,R);
vst[1&1][H_now]=true;
}
for(int i=2;i<=k;i++){
len=(ll)strlen(s[i]);
Hash::init(s[i],len);
vst[i&1].clear();
for(int L=0,R=L+LEN-1;R<len;R++,L++){
int H_now=Hash::get_hash(L,R);
if(vst[!(i&1)][H_now])
vst[i&1][H_now]=true;
}
}
if(!vst[k&1].empty())return true;
return false;
}
void solve() {
cin>>k;
ll l=0,r=2e5+7;
for(int i=1;i<=k;i++){
cin>>s[i];
r=min(r,(ll)strlen(s[i]));
}
while(l<r){
ll mid=(l+r+1)>>1;
if(yz(mid))
l=mid;
else
r=mid-1;
}
cout<<l<<endl;
}
signed main(){
#ifdef ONLINE_JUDGE
#else
freopen("test.in", "r", stdin);
freopen("test.out", "w", stdout);
#endif
ios::sync_with_stdio(false);//cin.tie(0);cout.tie(0);
// ll t;cin>>t;while(t--)
solve();
return 0;
}