题解:P14363 [CSP-S 2025] 谐音替换 / replace(民间数据)

· · 题解

前置知识

Trie字典树 树上差分 树状数组 扫描线思想

题目分析

题目给定 n 个字符串对 (s_{i,1},s_{i,2}),保证每对字符串长度相等。再给出 q 次询问,每次询问给定字符串对 (t_{j,1},t_{j,2}),问有多少个 (s_{i,1},s_{i,2}) 可以把 t_{j,1} 替换成 t_{j,2}.

首先让我们分析一下替换操作。

有以下性质:

  1. 替换后,字符串的长度不变。
  2. 替换后,原字符串(t_{j,1})与新字符串(t_{j,2})的最短不同区间的字符与替换操作使用的字符串对((s_{i,1},s_{i,2}))的最短不同区间的字符相同。

::::info[最短不同区间] 若将一对字符串的区间中的字符都删去后,字符串对相同,则记这个区间为不同区间,在所有的不同区间中的最短的那个,就记作最短不同区间

例如:

:::: 性质 $1$ 显然。对于性质 $2$,我们可以用感性理解:若一个旧字符串想通过**替换**操作变成一个新字符串,那么字符改变的部分即为**最短不同区间**的字符,必须与 $(s_{i,1},s_{i,2})$ 的**最短不同区间**的字符一致。 ### 思路分析 所以我们先对所有字符串对按照**最短不同区间**的字符分类,这一步可以用哈希(时间复杂度 $\text{O}(L+n\log{n})$),也可以用 $\text{Trie}$ 字典树(时间复杂度 $\text{O}(L)$)。 #### 以下我们再讨论同一类的字符串对(即最短不同区间中的字符相同的字符串对): 我们发现,如果一对字符串($(s_{i,1},s_{i,2})$)可以把原字符串($t_{j,1}$)替换成新字符串($t_{j,2}$),除了要求**最短不同区间**的字符相同之外,还要要求字符串对 $(s_{i,1},s_{i,2})$ 除**最短不同区间**之外的相同的字符在 $(t_{j,1},t_{j,2})$ 中出现。 于是对于**最短不同区间左侧**的相同部分,我们要求 $(s_{i,1},s_{i,2})$ 相同部分的是 $(t_{j,1},t_{j,2})$ 相同部分的后缀;对于**最短不同区间右侧**的相同部分,我们要求 $(s_{i,1},s_{i,2})$ 相同部分的是 $(t_{j,1},t_{j,2})$ 相同部分的前缀 。 题意至此简化为:给定 $n$ 个字符串对$(S_{i,1},S_{i,2})$,再询问 $q$ 次,每次给定字符串对 $(T_{j,1},T_{j,2})$,问有多少个 $(S_{i,1},S_{i,2})$ 满足 $S_{i,1}$ 是 $T_{j,1}$ 的后缀,而 $S_{i,2}$ 是 $T_{j,2}$ 的前缀。 在线处理询问显然不好做。 注意 $\text{Trie}$ 字典树处理的是前缀计数查询或者后缀计数查询,并不能支持同时满足两者条件的查询。 貌似这是一种另类的二维偏序? 于是可以把 $S_{i,1}$ 倒序插入一个字典树(记作 $L$ ),然后把 $S_{i,2}$ 顺序插入另一个字典树(记作 $R$ ),并在 $S_{i,1}$ 在 $L$ 上的节点上记录 $S_{i,2}$ 在 $R$ 上的节点编号。 如果记 $T_{j,1}$ 倒序插入 $L$ 所在的节点的编号为 $N_{j,1}$,$T_{j,2}$ 顺序插入 $R$ 所在的节点编号为 $N_{j,2}$,那么询问转化成问 $N_{j,1}$ 的所有树上祖先记录的 $R$ 的节点编号中有多少个是 $N_{j,2}$ 的祖先。 对于这类问题,离线处理,对 $L$ 进行扫描线操作,对 $R$ 进行树上差分,树状数组修改并查询即可。 ### 做法总结 1. 对于所有字符串对,先找到其**最短不同区间**的字符串对应的分类情况,如果对于询问操作给定的字符串对,没有找到对应的分类情况,那么就代表答案为 $0$. 2. 分类完后,对于每个分类情况再进行讨论: 1. 把这种分类情况下的所有 $(s_{j,1},s_{j,2})$ 的**最短不同区间左侧**的字符倒序插入这种分类情况下的 $L$,再把**右侧**的字符顺序插入这种分类情况下的 $R$,并在 $L$ 上的节点记录 $R$ 上的节点编号。 2. 把这种分类情况下的所有 $(t_{j,1},t_{j,2})$ 的**最短不同区间左侧**的字符倒序插入这种分类情况下的 $L$,再把**右侧**的字符顺序插入这种分类情况下的 $R$。 注意,这时我们插入字符的时候就**不能新开点**了,遇到走不了的字符,就停止。并在 $L$ 上的能走到的最深节点记录 $R$ 上的能走到的最深节点编号和对应的询问编号(为了更新答案)。 3. 遍历这种情况下的 $R$,记录 $\text{dfs}$ 的入序和出序(有个小优化,如果我们把 $R$ 上每个节点都当作真实的节点的话,$\text{dfs}$ 序的取值范围就会达到 $[1,L_1]$,而如果遍历到了一个节点,那个节点没有被 $L$ 上的记录,就不需要让它更新 $\text{dfs}$ 序,这样 $\text{dfs}$ 序的取值范围就优化到 $[1,n+q]$),为之后树上差分做准备。 4. 遍历这种情况下的 $L$,每次进到一个点,顺序执行以下操作: 1. 遍历这个点所记录的 $R$ 上的节点编号,将其子树内的所有点权用 $\text{dfs}$ 序和树状数组加 $1$. 2. 遍历这个点所记录的所有询问,用树状数组查询并更新答案。 3. 枚举儿子,递归。 4. 遍历这个点所记录的 $R$ 上的节点编号,撤销所有的加 $1$ 操作。 3. 讨论完所有分类情况,输出答案。 ### 时间复杂度 时间复杂度 $\text{O}(L_1+L_2+n\log{n})

代码

:::success[代码]

#include<bits/stdc++.h>
using namespace std;
namespace SOLVE{
#define N 200000
#define LEN 5000000
#define PII pair<int,int>
#define fi first
#define se second
int n,q;
char tmp[LEN+N*2+2],tmp2[LEN+N*2+2];
char *s1[N+1],*s2[N+1],*t1[N+1],*t2[N+1];
inline void input(){
    scanf("%d%d",&n,&q);
    int tpsz=1,tp2sz=1;
    int L1=0,L2=0;
    for(int i=1,len;i<=n;++i){
        scanf("%s",tmp+tpsz);
        len=strlen(tmp+tpsz);
        s1[i]=tmp+tpsz-1;
        tpsz+=len+1;
        L1+=len;
        scanf("%s",tmp+tpsz);
        len=strlen(tmp+tpsz);
        s2[i]=tmp+tpsz-1;
        tpsz+=len+1;
        L1+=len;
    }
    assert(L1<=5000000);
    for(int i=1,len;i<=q;++i){
        scanf("%s",tmp2+tp2sz);
        len=strlen(tmp2+tp2sz);
        t1[i]=tmp2+tp2sz-1;
        tp2sz+=len+1;
        L2+=len;
        scanf("%s",tmp2+tp2sz);
        len=strlen(tmp2+tp2sz);
        t2[i]=tmp2+tp2sz-1;
        tp2sz+=len+1;
        L2+=len;
    }
    assert(L2<=5000000);
}
int ans[N+1],p[N+1],p2[N+1];
int trK[LEN+1][26],numK[LEN+1],totK,sumK;
int slen[N+1];
int hdL[N+1],hdR[N+1];
vector<int> idR[LEN+N+1];
vector<PII> ask[LEN+N+1];
int trL[LEN+N+1][26],totL;
int trR[LEN+N+1][26],totR,dfn[LEN+N+1],dfu[LEN+N+1],dfc;
int vis[LEN+N+1];
struct BIT{
    int tr[2*N+2];
    inline void add(int k,int x){for(;k<=n+q+1;k+=k&-k)tr[k]+=x;}
    inline int query(int k){int ans=0;for(;k;k-=k&-k)ans+=tr[k];return ans;}
}bit;
inline void dfsR(int u){
    if(vis[u])++dfc;
    dfn[u]=dfc;
    for(int i=0;i<26;++i)if(trR[u][i])dfsR(trR[u][i]);
    dfu[u]=dfc;
}
inline void dfsL(int u){
    for(int id:idR[u]){
        bit.add(dfn[id],1);
        bit.add(dfu[id]+1,-1);
    }
    for(auto Q:ask[u]){
        ans[Q.se]=bit.query(dfn[Q.fi]);
    }
    for(int i=0;i<26;++i)if(trL[u][i])dfsL(trL[u][i]);
    for(int id:idR[u]){
        bit.add(dfn[id],-1);
        bit.add(dfu[id]+1,1);
    }
}
inline void solve(){
    for(int i=1;i<=n;++i){
        slen[i]=strlen(s1[i]+1);
        int l=1,r=slen[i];
        while(l<=r&&s1[i][l]==s2[i][l])++l;
        if(l>r)continue;
        while(l<=r&&s1[i][r]==s2[i][r])--r;
        for(int j=l,cur=1;j<=r;++j){
            if(!trK[cur][s1[i][j]-'a'])trK[cur][s1[i][j]-'a']=++totK;
            cur=trK[cur][s1[i][j]-'a'];
            if(!trK[cur][s2[i][j]-'a'])trK[cur][s2[i][j]-'a']=++totK;
            cur=trK[cur][s2[i][j]-'a'];
            if(j==r){
                if(!numK[cur])numK[cur]=++sumK;
                p[i]=numK[cur];
            }
        }
        if(!hdL[p[i]])hdL[p[i]]=++totL;
        int Lcur=hdL[p[i]];
        for(int j=l;--j;){
            if(!trL[Lcur][s1[i][j]-'a'])trL[Lcur][s1[i][j]-'a']=++totL;
            Lcur=trL[Lcur][s1[i][j]-'a'];
        }
        if(!hdR[p[i]])hdR[p[i]]=++totR;
        int Rcur=hdR[p[i]];
        for(int j=r+1;j<=slen[i];++j){
            if(!trR[Rcur][s1[i][j]-'a'])trR[Rcur][s1[i][j]-'a']=++totR;
            Rcur=trR[Rcur][s1[i][j]-'a'];
        }
        idR[Lcur].push_back(Rcur);
        vis[Rcur]=1;
    }
    for(int i=1;i<=q;++i){
        int tlen=strlen(t1[i]+1);
        if(tlen!=strlen(t2[i]+1))continue;
        int l=1,r=tlen;
        while(l<=r&&t1[i][l]==t2[i][l])++l;
        while(l<=r&&t1[i][r]==t2[i][r])--r;
        for(int j=l,cur=1;j<=r;++j){
            cur=trK[cur][t1[i][j]-'a'];
            if(!cur)break;
            cur=trK[cur][t2[i][j]-'a'];
            if(!cur)break;
            if(j==r){
                p2[i]=numK[cur];
            }
        }
        if(!p2[i])continue;
        int Lcur=hdL[p2[i]],Rcur=hdR[p2[i]];
        for(int j=l;--j;){
            if(!trL[Lcur][t1[i][j]-'a'])break;
            Lcur=trL[Lcur][t1[i][j]-'a'];
        }
        for(int j=r+1;j<=tlen;++j){
            if(!trR[Rcur][t1[i][j]-'a'])break;
            Rcur=trR[Rcur][t1[i][j]-'a'];
        }
        vis[Rcur]=1;
        ask[Lcur].push_back({Rcur,i});
    }
    for(int i=1;i<=sumK;++i){
        dfsR(hdR[i]);
        dfsL(hdL[i]);
    }
}
inline void output(){
    for(int i=1;i<=q;++i)printf("%d\n",ans[i]);
}
void main(){
    input();
    solve();
    output();
}
}
int main(){SOLVE::main();}

:::