Loading

串串从入门到入土

串串从入门到入土

Part.1 KMP算法

解决问题:匹配两个字符串 \(s\)\(t\)

核心:找 border(同时是 \(s[1,i]\) 前缀和后缀的最大长度)

for(int i=2,j=0;i<=n;i++)
{
    while(j&&p[j+1]!=p[i])j=ne[j];
    if(p[i]==p[j+1])j++;
    ne[i]=j;
}

\(ne[i]\) 表示 border 的长度,跳到 \(i+1\) 时,如果可以匹配就直接匹配,否则匹配 \(ne[i]\),因为 border 既是前缀又是后缀,现在能匹配到这里,跳到 \(ne[i]\) 显然也能匹配到。

复杂度是 \(O(n)\),也很显然,每次 \(j\) 最多加一,总共最多加 \(n\),所以 \(while\) 里的语句最多执行 \(n\) 次。

Part.2 Z 函数 (扩展 KMP)

解决问题:对每个 \(i\) 为左端点的后缀,求出 \(z[i]\) 即这个后缀和 整个串 \(s\) 的 LCP。

核心:设 \(l=i\)\(r=i+z[i]-1\),维护 \(r\) 最大的 \(l,r\)。如果 \(i\le r\),因为 \(s[l,r]=s[0,r-l]\),所以 \(s[0,i-l]=s[i,r]\)。所以继承一下 \(z[i-l]\) 之后暴力拓展就好了。

for(int i=2,l,r=0;i<=n;i++)
{
    if(i<=r)z[i]=min(r-i+1,z[i-l+1]);
    while(i+z[i]<=n&&s[1+z[i]]==s[i+z[i]])++z[i];
    if(i+z[i]-1>r)l=i,r=i+z[i]-1;
}

复杂度 \(O(n)\), 因为每次执行 \(while\) 都会让 \(r\) 加上一。

Part.3 AC 自动机

AC自动机,一开始听到名字很令人兴奋,然而此AC非彼AC啦。

解决问题:多模式匹配串,人话就是给你一个文本串 \(S\)\(n\) 个模式串 \(T_{1 \sim n}\),请你分别求出每个模式串 \(T_i\)\(S\) 中出现的次数。

\(n\) 遍 KMP 显然是不行的,复杂度爆炸了,怎么优化呢?想象一下我们把 \(n\) 个模式串拼成一坨东西,然后和\(S\) 快速地匹配。

\(n\) 个串拼成一坨东西这个操作听着有点熟悉,这不就是 Trie 的功能吗?

然后回想 KMP 时我们是怎么快速匹配的,靠着 \(ne\) 数组,我们可以在失配时迅速重新匹配。

同样地,在 AC自动机上建一个 \(fail\) 数组,失配后就可以跳到这里,\(fail\) 指向的是当前节点的最长后缀。

为什么 \(ne\) 指向的是又是前缀又是后缀的最大长度,而 \(fail\) 没有前缀这条限制呢?

其实 Trie 的结构已经保证了,我们指向的一个节点代表的就是一个模式串的前缀。

其实建议看 OI wiki 啦,AC自动机这块讲得超级好!

模板的代码👇

#include<bits/stdc++.h>
using namespace std;
const int N=2e6+10;
int n,tot,vis[N],ans[N],fail[N],tr[N][30];
char s[N];
vector<int>e[N],id[N];
void insert(int idx)
{
    int p=0,len=strlen(s);
    for(int i=0;i<len;i++)
    {
        if(!tr[p][s[i]-'a'])tr[p][s[i]-'a']=++tot;
        p=tr[p][s[i]-'a'];
    }
    id[p].push_back(idx);
    return;
}
queue<int>q;
void build()
{
    for(int i=0;i<26;i++)if(tr[0][i])e[0].push_back(tr[0][i]),q.push(tr[0][i]);
    while(!q.empty())
    {
        int p=q.front();
        q.pop();
        for(int i=0;i<26;i++)
        {
            if(tr[p][i])
            {
                fail[tr[p][i]]=tr[fail[p]][i];
                e[fail[tr[p][i]]].push_back(tr[p][i]);
                q.push(tr[p][i]);
            }
            else tr[p][i]=tr[fail[p]][i];
        }
    }
    return;
}
void query()
{
    int p=0,len=strlen(s);
    for(int i=0;i<len;i++)vis[tr[p][s[i]-'a']]++,p=tr[p][s[i]-'a'];
    return;
}
void dfs(int u)
{
    for(int v:e[u])dfs(v),vis[u]+=vis[v];
    for(int p:id[u])ans[p]=vis[u];
    return;
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)scanf("%s",s),insert(i);
    build();
    scanf("%s",s),query();
    dfs(0);
    for(int i=1;i<=n;i++)printf("%d\n",ans[i]);
    return 0;
}

模板的代码比简单版多了一个 \(dfs\),为啥呢?很简单,因为模板要统计每个模式串的出现次数,不能打上 \(tag\) 后跳过它。

所以选取另一种优化方式——建立 \(fail\) 树,即把每个点的 \(fail\) 看作它的父节点,然后再做子树和就ok了。

Part.4 后缀数组 (SA)

看起来很难,其实一点也不简单,主要是代码超级容易忘,所以要深刻地记住思路,才能不忘记代码!

先倍增,每个数以 \(rk[i]\) 以第一关键字,\(rk[i+w]\) 为第二关键字排序。

如果采用普通排序会多一个 \(\log\),观察到 \(rk[i]\) 的值域在 \(max(n,128)\) 里,所以可以上基数排序,复杂度 \(O(n\log n)\)了。

#include<bits/stdc++.h>
using namespace std;
const int N=2e6+10;
int n,m,w,id[N],ol[N],sa[N],rk[N],cnt[N];
char s[N];
int main()
{
    scanf("%s",s+1),n=strlen(s+1),m=128;
    for(int i=1;i<=n;i++)rk[i]=s[i],cnt[rk[i]]++;
    for(int i=1;i<=m;i++)cnt[i]+=cnt[i-1];
    for(int i=n;i>=1;i--)sa[cnt[rk[i]]--]=i;
    for(int w=1;w<n;w<<=1)
    {
        int tot=0;
        memcpy(id+1,sa+1,n*sizeof(int));
        for(int i=n-w+1;i<=n;i++)sa[++tot]=i;
        for(int i=1;i<=n;i++)if(id[i]>w)sa[++tot]=id[i]-w;
        memcpy(id+1,sa+1,n*sizeof(int));
        memset(cnt,0,sizeof(cnt));
        for(int i=1;i<=n;i++)cnt[rk[i]]++;
        for(int i=1;i<=m;i++)cnt[i]+=cnt[i-1];
        for(int i=n;i>=1;i--)sa[cnt[rk[id[i]]]--]=id[i];
        memcpy(ol+1,rk+1,n*sizeof(int)),m=0;
        for(int i=1;i<=n;i++)
        {
            if(ol[sa[i-1]]==ol[sa[i]]&&ol[sa[i-1]+w]==ol[sa[i]+w])rk[sa[i]]=rk[sa[i-1]];
            else rk[sa[i]]=++m;
        }
    }
    for(int i=1;i<=n;i++)printf("%d ",sa[i]);
    return 0;
}

Part.5 SAM (后缀自动机)

我会用SA写SAM模板题!

代码:

#include<bits/stdc++.h>
using namespace std;
const int N=2e6+10;
int n,m,w,id[N],ol[N],sa[N],rk[N],cnt[N],height[N],st[N],L[N],R[N];
long long ans;
char s[N];
int main()
{
    scanf("%s",s+1),n=strlen(s+1),m=128;
    for(int i=1;i<=n;i++)rk[i]=s[i],cnt[rk[i]]++;
    for(int i=1;i<=m;i++)cnt[i]+=cnt[i-1];
    for(int i=n;i>=1;i--)sa[cnt[rk[i]]--]=i;
    for(int w=1;w<n;w<<=1)
    {
        int tot=0;
        memcpy(id+1,sa+1,n*sizeof(int));
        for(int i=n-w+1;i<=n;i++)sa[++tot]=i;
        for(int i=1;i<=n;i++)if(id[i]>w)sa[++tot]=id[i]-w;
        memcpy(id+1,sa+1,n*sizeof(int));
        memset(cnt,0,sizeof(cnt));
        for(int i=1;i<=n;i++)cnt[rk[i]]++;
        for(int i=1;i<=m;i++)cnt[i]+=cnt[i-1];
        for(int i=n;i>=1;i--)sa[cnt[rk[id[i]]]--]=id[i];
        memcpy(ol+1,rk+1,n*sizeof(int)),m=0;
        for(int i=1;i<=n;i++)
        {
            if(ol[sa[i-1]]==ol[sa[i]]&&ol[sa[i-1]+w]==ol[sa[i]+w])rk[sa[i]]=rk[sa[i-1]];
            else rk[sa[i]]=++m;
        }
    }
    for(int i=1;i<=n;i++)rk[sa[i]]=i;
    for(int i=1,k=0;i<=n;i++)
    {
        if(rk[i]==1)continue;
        if(k)k--;
        while(i+k<=n&&sa[rk[i]-1]+k<=n&&s[i+k]==s[sa[rk[i]-1]+k])k++;
        height[rk[i]]=k;
    }
    for(int i=1,tot=0;i<=n;i++)
    {
        while(tot&&height[st[tot]]>=height[i])tot--;
        L[i]=st[tot],st[++tot]=i;
    }
    for(int i=n,tot=0;i>=1;i--)
    {
        while(tot&&height[st[tot]]>height[i])tot--;
        R[i]=st[tot],st[++tot]=i;
        if(!R[i])R[i]=n+1;
    }
    for(int i=1;i<=n;i++)ans=max(ans,1ll*height[i]*(R[i]-L[i]));
    printf("%lld",ans);
    return 0;
}

言归正传,sam到底是什么东西呢,其实就是把一个字符串的子串都插到一个自动机里去,让每个点维护了一个子串和它连续的若干个后缀就行了。

#include<bits/stdc++.h>
using namespace std;
const int N=2e6+10;
int n,id=1,la=1;
char s[N];
struct sam{
    int len,link,son[30],sz;
}st[N];
int idx,hd[N],to[N],nxt[N];
void add(int u,int v)
{
    to[++idx]=v,nxt[idx]=hd[u],hd[u]=idx;
    return;
}
void insert(int c)
{
    int cur=++id,p=la;
    st[cur].len=st[p].len+1,st[cur].sz=1;
    while(p&&!st[p].son[c])st[p].son[c]=cur,p=st[p].link;
    if(!p)st[cur].link=1;
    else
    {
        int q=st[p].son[c];
        if(st[q].len==st[p].len+1)st[cur].link=q;
        else
        {
            int clone=++id;
            st[clone].len=st[p].len+1;
            st[clone].link=st[q].link;
            for(int i=0;i<26;i++)st[clone].son[i]=st[q].son[i];
            while(p&&st[p].son[c]==q)st[p].son[c]=clone,p=st[p].link;
            st[q].link=st[cur].link=clone;
        }
    }
    la=cur;
    return;
}
long long ans;
void dfs(int u)
{
    for(int i=hd[u];i;i=nxt[i])if(to[i])dfs(to[i]),st[u].sz+=st[to[i]].sz;
    if(st[u].sz!=1)ans=max(ans,1ll*st[u].sz*st[u].len);
    return;
}
int main()
{
    scanf("%s",s+1),n=strlen(s+1);
    for(int i=1;i<=n;i++)insert(s[i]-'a');
    for(int i=2;i<=id;i++)add(st[i].link,i);
    dfs(1);
    printf("%lld",ans);
    return 0;
}
posted @ 2025-08-01 13:02  AvisD  阅读(35)  评论(3)    收藏  举报