失配树

注:本文中字符串下标均从 1 开始。

先看一个简单的问题:

给出一个字符串 S,求 S 两个长度分别为 nm 的前缀的最长公共 border 长度。

A:我暴力找!

1n,m|S|106

A:我加个哈希!

T 组询问,T105

A:……

此时就需要我们的失配树了。

我们先来看一组样例:

假设这是我们找出的一个字符串的两个前缀。我们可以发现它们的最长公共 border 长度是 3

说到求 border,怎么少的了我 KMP 呢?既然两个字符串都是同一个串的前缀,那么其中一个(较短的)必定也是另一个(较长的)字符串的前缀。

然后我们将其中较长的一个字符串的字符下标及其 nxt 数组列出来看看。

nxt102031425364

能看出什么东西吗?不能?那我们建成一棵树看看?

接下来,我们连边 (nxt[i],i),i[1,6]

然后是这个样子:

长度为 4 和长度为 6 的后缀的最长公共 border 长为 2,在树上的关系是什么?

可以看出,246 除了自己之外的 LCA。

这棵树也就是所谓的失配树,通过对于一个字符串建出这棵树,我们可以快速找出其多组长度不同的前缀的最长公共 border 长度。

解释一下原理:

如果 CB 的 border,BA 的 border,那么 CA 的 border。

也就是说处理出 nxt 数组之后,A 可以不断跳 nxtBB 也可以不断跳 nxtC

所以说,如果两个前缀能通过跳 nxt 跳到同一个位置去,那么第一个跳到的相同的位置就是它们的最长公共 border 长度。

这个过程和我们树上找 LCA 的方式很像,所以我们可以将其建成一棵树。

然后可以选择倍增或树剖等其他方式来跳。

我写了树剖求 LCA 结果 TLE 了,于是改了倍增,代码如下:

#include<queue>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<iostream>
#include<algorithm>
#define maxn 1000100
#define rr register 
#define INF 0x3f3f3f3f
//#define int long long

using namespace std;

char s[maxn];
int m,n,j,tot,f[maxn][40];
int nxt[maxn],head[maxn],dep[maxn];
struct edge{int fr,to,nxt;}e[maxn];

int read(){
    int s=0,w=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
    while(ch>='0'&&ch<='9')s=(s<<1)+(s<<3)+ch-'0',ch=getchar();
    return s*w;
}

void add(int fr,int to){
    e[++tot]=(edge){fr,to,head[fr]};head[fr]=tot;
}

int dfs(int u){
    for(rr int i=1;i<=21;i++)
        f[u][i]=f[f[u][i-1]][i-1];
    for(rr int i=head[u];i;i=e[i].nxt){
        int to=e[i].to;
        dep[to]=dep[u]+1,f[to][0]=u;
        dfs(to);
    }
}

int GetLCA(int x,int y){
    if(dep[x]<dep[y]) swap(x,y);
    for(rr int i=21;i>=0;i--)
        if(dep[f[x][i]]>=dep[y])x=f[x][i]; 
    for(rr int i=21;i>=0;i--)
        if(f[x][i]!=f[y][i])x=f[x][i],y=f[y][i];    
    return f[x][0];
}

int main(){
    scanf("%s",s+1);n=strlen(s+1);
    for(rr int i=1;i<n;i++){
        while(j&&s[j+1]!=s[i+1]) j=nxt[j];
        if(s[j+1]==s[i+1]) j++,nxt[i+1]=j;
    }
    for(rr int i=1;i<=n;i++) add(nxt[i],i);
    dfs(0);m=read();
    for(rr int i=1,fr,to;i<=m;i++){
        fr=read();to=read();
        printf("%d\n",GetLCA(fr,to));
    }
    return 0;
}

我原以为树剖求 LCA 会超时是因为跳的太慢被卡了,后来发现不是的。

经过 @Suzt_ilymtics 大佬的研究发现,因为我们建出来的失配树根节点一定是 0,而我们的 son 初值是 0,也就相当于所有点的重儿子一开始都是根节点,显然不对。

另外 siz[0] 可能是很大的,可能导致剖分时找不到重儿子。所以我们将 son 的初值设为一个大于 n 的值就可以避免那种情况。

结果改完后实测比倍增快了 8s+。我就知道我树剖不会被卡。

树剖的代码

#include<queue>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<iostream>
#include<algorithm>
#define maxn 1000100
#define rr register 
#define INF 0x3f3f3f3f
//#define int long long

using namespace std;

char s[maxn];
int m,n,j,tot;
int nxt[maxn],head[maxn];
struct edge{int fr,to,nxt;}e[maxn];

int read(){
    int s=0,w=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
    while(ch>='0'&&ch<='9')s=(s<<1)+(s<<3)+ch-'0',ch=getchar();
    return s*w;
}

void add(int fr,int to){
    e[++tot]=(edge){fr,to,head[fr]};head[fr]=tot;
}

namespace Cut{
    int siz[maxn],dep[maxn];
    int fa[maxn],son[maxn],top[maxn];
    void dfs1(int u,int fat){
        dep[u]=dep[fat]+1;
        siz[u]=1;fa[u]=fat;
        for(rr int i=head[u];i;i=e[i].nxt){
            int to=e[i].to;
            if(to==fat) continue;
            dfs1(to,u);siz[u]+=siz[to];
            if(siz[son[u]]<siz[to]) son[u]=to;
        }
    } 
    
    void dfs2(int u,int tp){
        top[u]=tp;
        if(son[u]!=n+5) dfs2(son[u],tp);//this.
        for(rr int i=head[u];i;i=e[i].nxt){
            int to=e[i].to;
            if(to==son[u]||to==fa[u]) continue;
            dfs2(to,to);
        }
    }
    
    int GetLCA(int x,int y){
        int fir=x,sec=y,ans;
        while(top[x]!=top[y]){
            if(dep[top[x]]<dep[top[y]]) swap(x,y);
            x=fa[top[x]];
        }
        ans=dep[x]<dep[y]?x:y;
        if(ans==fir||ans==sec) return fa[ans];
    }
}

int main(){
    scanf("%s",s+1);n=strlen(s+1);
    for(rr int i=1;i<n;i++){
        while(j&&s[j+1]!=s[i+1]) j=nxt[j];
        if(s[j+1]==s[i+1]) j++,nxt[i+1]=j;
    }
    for(rr int i=0;i<=n;i++) Cut::son[i]=n+5;//and this.
    for(rr int i=1;i<=n;i++) add(nxt[i],i);
    Cut::dfs1(0,-1);Cut::dfs2(0,0);
    
    m=read();
    for(rr int i=1,fr,to;i<=m;i++){
        fr=read();to=read();
        printf("%d\n",Cut::GetLCA(fr,to));
    }
    return 0;
}
posted @   KnightL  阅读(609)  评论(2编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示
主题色彩