字符串

P3370 【模板】字符串哈希

::::info[题目大意]
给定 \(n\) 个字符串(第 \(i\) 个字符串长度为 \(s_i\),字符串内包含数字、大小写字母,大小写敏感),请求出 \(n\) 个字符串中共有多少个不同的字符串。
::::
对每个字符串哈希,判断哈希值中有多少个不同的数字即可。

注意:程序中的哈希值会自然溢出,不会爆 long long

#include<bits/stdc++.h>
using namespace std;
#define int unsigned long long
int get_hash(string s)
{
    int res=0;
    for(int i=0;i<s.size();i++) res=res*13331+s[i];
    return res;
}
const int N=1e4+5;
int n,a[N],ans;
string s;
signed main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>s;
        a[i]=get_hash(s);
    }
    sort(a+1,a+1+n);
    for(int i=1;i<=n;i++)
    {
        if(i==1||a[i]!=a[i-1])
        ans++;
    }
    cout<<ans;
    return 0;
}

P1368 工艺

::::info[题目大意]
给定长度为 \(n\) 的整数序列,每次操作只能将序列最左端数字移至末尾,可进行任意次该操作得到原序列的所有循环移位排列,需要找出其中字典序最小的序列并完整输出。
::::
最小表示法模板题,看到循环很容易想到先破环成链,随后可以考虑双指针:定义两个候选起点分别为 \(i,j\),当前两个起点向后匹配的公共前缀长度为 \(k\)

  • 当前位相等\(a_{i+k}=a_{j+k}\),让 \(k\) 的值加 \(1\)
  • 当前位不等
    • \(a_{i+k} \gt a_{j+k}\),则 \(i,i+1,\cdots,i+k\) 都不可能是最优起点,让 \(i\) 的值变成 \(i+k+1\)
    • \(a_{i+k} \lt a_{j+k}\),则 \(j,j+1,\cdots,j+k\) 都不可能是最优起点,让 \(j\) 的值变成 \(j+k+1\)
  • \(i\)\(j\) 重合:让 \(j\) 的值变成 \(i+1\)

最终的最优起点为 \(\min(i,j)\),不难证明该算法的时间复杂度是 \(O(n)\) 的。

数组要开 \(2\) 倍大,要注意写法,不然复杂度可能会退化成 \(O(n^2)\)

#include<bits/stdc++.h>
using namespace std;
const int N=6e5+5;
int n,a[N];
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        a[i+n]=a[i];
    }
    int s1=1,s2=2,k=0;
    while(s1<=n&&s2<=n&&k<n)
    {
        if(a[s1+k]==a[s2+k]) k++;
        else
        {
            if(a[s1+k]>a[s2+k]) s1+=k+1;
            else if(a[s1+k]<a[s2+k]) s2+=k+1;
            if(s1==s2) s2++;
            k=0;
        }
    }
    for(int i=min(s1,s2);i<=min(s1,s2)+n-1;i++) cout<<a[i]<<" ";
    return 0;
}

P3805 【模板】Manacher

::::info[题目大意]
给出一个只由小写英文字符 \(\texttt a,\texttt b,\texttt c,\ldots\texttt y,\texttt z\) 组成的长度为 \(n\) 的字符串 \(S\) ,求 \(S\) 中最长回文串的长度 。
::::
马拉车模板题,首先我们会发现长度为奇数的回文串和长度为偶数的回文串的对称中心是不一样的,所以我们可以考虑在每两个相邻的字符之间插入一个字符 #,这样所有回文串的中心都会是一个字符了。

我们定义 \(p_i\) 表示以 \(i\) 为对称中心最多能扩展的距离,\(mr\) 表示目前我们经过的最靠右的点,\(mid\) 表示 \(mr\) 是由哪个对称中心扩展过去的。

暴力计算 \(p_i\)\(O(n^2)\) 的,显然无法通过。我们会注意到有很多 \(p_i\) 是没必要扩展的,我们可以分以下几类讨论:

  • \(i \le mr\):设 \(j\) 是以 \(mid\) 为对称中心时 \(i\) 的对称点。因为 \(\left[2 \times mid-mr,mr\right]\) 是回文的,所以 \(\left[j-p_j,j+p_j\right]\)\(\left[i-p_j,i+p_j\right]\) 是相等的。但这在 \(i+p_j \gt mr\) 时就不一定是对的,所以我们可以直接令 \(p_i\) 的值为 \(\min(p_j,mr-i+1)\) 后暴力扩展。
  • \(mid \lt i\):暴力扩展即可。

为了处理边界情况,我们可以在字符串前后各加上一个字符。因为 \(mr\)\(mid\) 都是不断右移的,所以时间复杂度为 \(O(n)\)

#include<bits/stdc++.h>
using namespace std;
const int N=3e7+5;
string s,t;
int n,mr,mid,p[N],ans;
signed main()
{
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    cin>>t;
    int n=t.size();
    for(int i=0;i<n;i++)
    {
        s+=t[i];
        s+="#";
    }
    s="~#"+s+"%";
    int len=s.size()-2;
    for(int i=1;i<=len;i++)
    {
        if(i<=mr) p[i]=min(p[2*mid-i],mr-i+1);
        else p[i]=1;
        while(s[i+p[i]]==s[i-p[i]]) p[i]++;
        if(i+p[i]-1>mr) mr=i+p[i]-1,mid=i;
        ans=max(ans,p[i]-1);
    }
    cout<<ans;
    return 0;
}

P3375 【模板】KMP

::::info[题目大意]
给出两个字符串 \(s_1\)\(s_2\),若 \(s_1\) 的区间 \([l, r]\) 子串与 \(s_2\) 完全相同,则称 \(s_2\)\(s_1\) 中出现了,其出现位置为 \(l\)
现在请你求出 \(s_2\)\(s_1\) 中所有出现的位置。

定义一个字符串 \(s\) 的 border 为 \(s\) 的一个\(s\) 本身的子串 \(t\),满足 \(t\) 既是 \(s\) 的前缀,又是 \(s\) 的后缀。
对于 \(s_2\),你还需要求出对于其每个前缀 \(s'\) 的最长 border \(t'\) 的长度。
::::
其实在每一次匹配过程中,我们可以判断后续几次匹配是否会成功,算法的核心就是每次匹配过程中推断出后续完全不可能匹配成功的匹配过程,从而减少比较的趟数。

如图,我们第一次比较过后,就可以直接跳到第四趟再进行判断了。因为主串中 \(3\)\(4\) 的位置就相当于模式串中一个子串的后缀了。

模式串出现了重复单元,在第一次匹配失败后就可以直接跳跃到出现重复单元的位置。

\(next\) 数组就是找出模式串中前后字符重复出现的个数,为了能够跳跃不可能匹配的步骤。

构建 \(next\) 数组

我们知道了 \(next_1\)\(next_2\)\(\dots\)\(next_{x-1}\),那么我们怎么求出 \(next_x\) 呢?我们设 \(next_{x-1} = y\)。那么,如果 \(a_x=a_{y+1}\),那么 \(next_x=next_{x-1}+1\)。因为前缀和后缀都多了一位(相同)。

\(a_{y+1}\)\(a_x\) 不相等的时候,我们需要缩小 \(y\),把 \(y\) 变成 \(next_{y-1}\),直到 \(a_{y+1}=a_x\) 为止。然后就可以像刚才一样递推了。

个人感觉 kmp 很难理解。

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int slen,tlen,nxt[N];
string s,t;
signed main()
{
    cin>>s>>t;
    slen=s.size(),tlen=t.size(),s=" "+s,t=" "+t;
    for(int i=2,j=0;i<=tlen;i++)
    {
        while(j&&t[j+1]!=t[i]) j=nxt[j];
        if(t[j+1]==t[i]) j++;
        nxt[i]=j;
    }
    for(int i=1,j=0;i<=slen;i++)
    {
        while(j&&t[j+1]!=s[i]) j=nxt[j];
        if(t[j+1]==s[i]) j++;
        if(j==tlen)
        {
            cout<<i-tlen+1<<endl;
            j=nxt[j];
        }
    }
    for(int i=1;i<=tlen;i++) cout<<nxt[i]<<" ";
    return 0;
}

P5410 【模板】扩展 KMP / exKMP(Z 函数)

::::info[题目大意]
给定两个字符串 \(a,b\),你要求出两个数组:

  • \(b\)\(z\) 函数数组 \(z\),即 \(b\)\(b\) 的每一个后缀的 LCP 长度。
  • \(b\)\(a\) 的每一个后缀的 LCP 长度数组 \(p\)

对于一个长度为 \(n\) 的数组 \(a\),设其权值为 \(\operatorname{xor}_{i=1}^n i \times (a_i + 1)\)

注意:LCP 指的是最长公共前缀。
::::
首先,我们可以很轻松的写出一个暴力代码:

while(i+z[i]<=n&&s[i+z[i]]==s[1+z[i]]) z[i]++;

我们不难发现这个暴力代码和 Manacher 很像,所以我们可以用类似于 Manacher 的方法进行优化。记录目前匹配到的右端点最靠右的区间 \(\left[lt,rt\right]\),枚举 \(i\),可以分类讨论:

  • \(i \le rt\):根据定义有区间 \(\left[1,rt-lt+1\right]\) 和区间 \(\left[lt,rt\right]\) 是相同的,所以 \(z_i=z_{i-lt+1}\)。但这在 \(i+z_{i-lt+1}-1 \gt rt\) 的时候就不一定了,所以我们可以先令 \(z_i=\min(z_{i-lt+1},rt-i+1)\),然后暴力扩展。
  • \(i \gt rt\):暴力扩展即可。

解决第二小问的方法就更巧妙了,我们只需要令一个字符串 \(s\)b#a,则 \(p_i\) 的值就为 \(z_{lenb+1+i}\),其中 \(lenb\) 是字符串 \(b\) 的长度。

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=4e7+5;
int z[N],len_a,len_b,ans_z,ans_p;
string a,b;
void calc(string s,int len)
{
    memset(z,0,sizeof(z));
    z[1]=len;
    int lt=1,rt=1;
    for(int i=2;i<=len;i++)
    {
        if(i<=rt) z[i]=min(z[i-lt+1],rt-i+1);
        while(i+z[i]<=len&&s[i+z[i]]==s[1+z[i]]) z[i]++;
        if(i+z[i]-1>rt) lt=i,rt=i+z[i]-1;
    }
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin>>a>>b;
    len_a=a.size(),len_b=b.size();
    calc(" "+b,len_b);
    for(int i=1;i<=len_b;i++) ans_z^=i*(z[i]+1);
    calc(" "+b+"#"+a,len_b+1+len_a);
    for(int i=1;i<=len_a;i++) ans_p^=i*(z[len_b+1+i]+1);
    cout<<ans_z<<endl<<ans_p;
    return 0;
}

P2580 于是他错误的点名开始了

::::info[题目大意]
先给定一共 \(n\) 个互不重复的学生姓名,随后给出教练依次点出的 \(m\) 个姓名,逐个判断每一个点名名字的情况并输出对应结果:

  • 名字在名单里,本次是第一次被点到:输出 OK
  • 名字在名单里,之前已经点过:输出 REPEAT
  • 名字根本不在名单中:输出 WRONG
    ::::
    简单题,用一个哈希表就可以了,但这里为了复习字符串还是给出字典树解法。先对 \(n\) 个名字造出字典树,并对每个节点记录当前状态。对于每次点名,如果没有这个点就输出 WRONG,否则根据节点状态判断是否点过名即可。
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5;
int n,m,vis[N],tr[N][25],cnt;
void insert(string s)
{
    int len=s.size(),cur=0;
    for(int i=0;i<len;i++)
    {
        if(!tr[cur][s[i]-'a']) tr[cur][s[i]-'a']=++cnt;
        cur=tr[cur][s[i]-'a'];
    }
    vis[cur]=1;
}
int query(string s)
{
    int len=s.size(),cur=0;
    for(int i=0;i<len;i++)
    {
        if(!tr[cur][s[i]-'a']) return 0;
        cur=tr[cur][s[i]-'a'];
    }
    if(!vis[cur]) return 0;
    else if(vis[cur]==1){vis[cur]=2;return 1;}
    else return 2;
}
string s;
signed main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>s;
        insert(s);
    }
    cin>>m;
    for(int i=1;i<=m;i++)
    {
        cin>>s;
        int ans=query(s);
        if(ans==0) cout<<"WRONG\n";
        else if(ans==1) cout<<"OK\n";
        else cout<<"REPEAT\n";
    }
    return 0;
}

P3808 AC 自动机(简单版)

::::info[题目大意]
给定 \(n\) 个模式串 \(s_i\) 和一个文本串 \(t\),求有多少个不同的模式串在文本串里出现过。
两个模式串不同当且仅当他们编号不同。
::::
首先,我们先将这 \(n\) 个模式串建成一个字典树。

AC自动机

假如我们现在有文本串 ABCDBC。我们用文本串在 Trie 上匹配,刚开始会经过 \(2,3,4\)号点,发现到了 \(4\),成功地匹配了一个模式串,然后就不能再继续匹配了。这时如果回到根节点重新匹配的话效率就太慢了,我们可以借鉴 KMP 的思想,在字典树上的另一个节点继续跑。

明显在这颗字典树上,我们可以继续从 \(7\) 号点开始匹配,然后匹配到 \(8\)。那么我们怎么确定从那个点开始匹配呢?如果 \(i\) 匹配失败后继续从 \(j\) 开始匹配,我们就称 \(j\)\(i\) 的失配指针。

我们仔细想一下失配指针的本质是什么,如果一个点 \(i\) 的失配指针是 \(j\),那么从根节点到 \(j\) 的字符串一定是根节点到 \(i\) 的字符串的后缀。但如果有多个满足条件的 \(j\) 怎么办?我们可以让失配指针指向深度最大的那一个点。

怎么求失配指针呢?首先我们可以确定,每一个点的失配指针的深度一定比这个点的深度小。而第一层的点的失配指针一定是根节点。如果一个点 \(i\) 的父亲 \(fa\) 的失配指针是 \(k\),且 \(k\) 有一个和 \(i\) 的值相同的儿子,那么让 \(i\) 的失配指针指向那个儿子。因为这个是按层数从小到大遍历的,所以我们可以考虑使用 BFS。

这里有一些实现细节:

  • 如果不存在一个点 \(i\),我们可以将那个节点设为 \(k\) 的儿子中和 \(i\) 的值相同的那个点,因为你迟早就要跳到那里。
  • 无论 \(k\) 存不存在和 \(i\) 的值相同的儿子,我们都可以将 \(i\) 的值指向那个点,因为在之前那个点就肯定已经处理好了(见上一条细节)。

查询时,我们就只需要在只需要正常在字典树上走,过程中一直在失配指针上跳就行了。

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e6+5;
int n,fail[N],tr[N][26],vis[N],sum[N],cnt;
void insert(string s)
{
    int len=s.size(),cur=0;
    for(int i=0;i<len;i++)
    {
        if(!tr[cur][s[i]-'a']) tr[cur][s[i]-'a']=++cnt;
        cur=tr[cur][s[i]-'a'];
    }
    sum[cur]++;
}
void build()
{
    queue<int> q;
    for(int i=0;i<26;i++)
    {
        if(tr[0][i])
        q.push(tr[0][i]);
    }
    while(q.size())
    {
        int u=q.front();
        q.pop();
        for(int i=0;i<26;i++)
        {
            if(tr[u][i])
            {
                fail[tr[u][i]]=tr[fail[u]][i];
                q.push(tr[u][i]);
            }
            else tr[u][i]=tr[fail[u]][i];
        }
    }
}
int query(string s)
{
    int len=s.size(),cur=0,res=0;
    for(int i=0;i<len;i++)
    {
        cur=tr[cur][s[i]-'a'];
        int j=cur;
        while(j&&!vis[j]) res+=sum[j],vis[j]=true,j=fail[j];
    }
    return res;
}
string s;
signed main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>s;
        insert(s);
    }
    build();
    cin>>s;
    cout<<query(s);
    return 0;
}

P3796 AC 自动机(简单版 II)

::::info[题目大意]
\(N\) 个由小写字母组成的模式串以及一个文本串 \(T\)。每个模式串可能会在文本串中出现多次。你需要找出哪些模式串在文本串 \(T\) 中出现的次数最多。

题目保证模式串互不相同。
::::
题目都保证了模式串互不相同,我们就直接在字典树上的每个节点 \(i\) 都记录从根到点 \(i\) 的路径上的字符串对应的编号,然后直接跟上一题一样记录每个点被统计的次数即可。

注意:这里不能用 \(vis\) 数组了,因为这里每个点可能多次对答案产生贡献。

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=2e4+5;
int n,fail[N],tr[N][26],idx[N],cnt,sum[N];
void insert(string s,int id)
{
    int len=s.size(),cur=0;
    for(int i=0;i<len;i++)
    {
        if(!tr[cur][s[i]-'a']) tr[cur][s[i]-'a']=++cnt;
        cur=tr[cur][s[i]-'a'];
    }
    idx[cur]=id;
}
void build()
{
    queue<int> q;
    for(int i=0;i<26;i++)
    {
        if(tr[0][i])
        q.push(tr[0][i]);
    }
    while(q.size())
    {
        int u=q.front();
        q.pop();
        for(int i=0;i<26;i++)
        {
            if(tr[u][i])
            {
                fail[tr[u][i]]=tr[fail[u]][i];
                q.push(tr[u][i]);
            }
            else tr[u][i]=tr[fail[u]][i];
        }
    }
}
void query(string s)
{
    int len=s.size(),cur=0,res=0;
    for(int i=0;i<len;i++)
    {
        cur=tr[cur][s[i]-'a'];
        int j=cur;
        while(j) sum[idx[j]]++,j=fail[j];
    }
}
string s[N];
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    while(cin>>n)
    {
        if(n==0) return 0;
        memset(tr,0,sizeof(tr));
        memset(sum,0,sizeof(sum));
        memset(fail,0,sizeof(fail));
        memset(idx,0,sizeof(idx));
        cnt=0;
        for(int i=1;i<=n;i++)
        {
            cin>>s[i];
            insert(s[i],i);
        }
        build();
        cin>>s[0];
        query(s[0]);
        int ans=0;
        for(int i=1;i<=n;i++) ans=max(ans,sum[i]);
        cout<<ans<<"\n";
        for(int i=1;i<=n;i++)
        {
            if(sum[i]==ans)
            cout<<s[i]<<'\n';
        }
    }
    return 0;
}

P5357 【模板】AC 自动机

::::info[题目大意]
给你一个文本串 \(S\)\(n\) 个模式串 \(T_{1 \sim n}\),请你分别求出每个模式串 \(T_i\)\(S\) 中出现的次数。

数据不保证任意两个模式串不相同
::::
这道题看起来很模板,但当打上之前的代码后……超时了!我们思考一下为什么,由于我们每次暴力跳失配指针,每次跳深度都可能只减 \(1\),所以它的最劣时间复杂度是模式串长度乘以文本串长度。

我们分析一下原代码的瓶颈——每次暴力跳失配指针,如果我们有一个办法可以让每个节点都只被经过一次就能 AC 了。不难发现,如果每一个点 \(i\) 都向他们的失配指针 \(fail_i\) 连一条有向边,那么我们最终就会得到一个根节点为 \(0\) 的树,每个点都连向了自己的父亲。

那么,每次从一个点开始在失配指针上暴力统计,其实就是让根节点到那个点的路径上的所有点全部加上 \(1\)。我们可以直接用树上差分完成,但这样写还是太麻烦了。因为树是一个有向无环图,所以我们直接用拓扑排序就行了。

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e6+5;
int n,fail[N],tr[N][26],idx[N],cnt,sum[N],in[N];
int insert(string s,int id)
{
    int len=s.size(),cur=0;
    for(int i=0;i<len;i++)
    {
        if(!tr[cur][s[i]-'a']) tr[cur][s[i]-'a']=++cnt;
        cur=tr[cur][s[i]-'a'];
    }
    return cur;
}
void build()
{
    queue<int> q;
    for(int i=0;i<26;i++)
    {
        if(tr[0][i])
        q.push(tr[0][i]);
    }
    while(q.size())
    {
        int u=q.front();
        q.pop();
        for(int i=0;i<26;i++)
        {
            if(tr[u][i])
            {
                fail[tr[u][i]]=tr[fail[u]][i];
                in[fail[tr[u][i]]]++;
                q.push(tr[u][i]);
            }
            else tr[u][i]=tr[fail[u]][i];
        }
    }
}
void query(string s)
{
    int len=s.size(),cur=0,res=0;
    for(int i=0;i<len;i++)
    {
        cur=tr[cur][s[i]-'a'];
        sum[cur]++;
    }
}
void topo()
{
    queue<int> q;
    for(int i=0;i<=cnt;i++)
    {
        if(in[i]==0)
        q.push(i);
    }
    while(q.size())
    {
        int u=q.front();
        q.pop();
        sum[fail[u]]+=sum[u];
        if((--in[fail[u]])==0) q.push(fail[u]);
    }
}
string s[N];
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>s[i];
        idx[i]=insert(s[i],i);
    }
    build();
    cin>>s[0];
    query(s[0]);
    topo();
    for(int i=1;i<=n;i++) cout<<sum[idx[i]]<<'\n';
    return 0;
}

P1659 [国家集训队] 拉拉队排练

::::info[题目大意]
询问一个字符串中所有奇回文按照长度降序排列,前k个奇回文的长度乘积。

\(n \le 10^6,k \le 10^{12}\)
::::
注意到 \(n\)\(k\) 都很大,不能将所有长度求出来后暴力计算。但是这个排序看起来似乎没有什么好的处理方法……吗?因为长度最大是 \(n\),所以我们可以考虑桶排。所以我们只需要用 Manacher 跑出长度为 \(i\) 的奇回文串的数量即可,有些实现细节:

  • 因为只统计奇回文,所以字符之间不需要插入 #
  • 因为长度为 \(n\) 的回文串里面也包含长度为 \(n-2,n-4,\cdots,1\) 的回文串,所以我们可以用类似于前缀和的方法来求解。
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e6+5,mod=19930726;
string s;
int n,k,mr,mid,p[N],ans=1,sum[N];
int qpow(int a,int b)
{
    int res=1;
    while(b)
    {
        if(b&1) res=(res*a)%mod;
        a=(a*a)%mod,b>>=1;
    }
    return res;
}
signed main()
{
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    cin>>n>>k>>s;
    s="^"+s+"%";
    for(int i=1;i<=n;i++)
    {
        if(i<=mr) p[i]=min(p[2*mid-i],mr-i+1);
        else p[i]=1;
        while(s[i+p[i]]==s[i-p[i]]) p[i]++;
        if(i+p[i]-1>mr) mr=i+p[i]-1,mid=i;
        sum[2*p[i]-1]++;
    }
    for(int i=n;i>=1;i--)
    {
        sum[i]+=sum[i+2];
        if(k>=sum[i]) ans=(ans*qpow(i,sum[i]))%mod,k-=sum[i];
        else{ans=(ans*qpow(i,k))%mod;k=0;break;}
    }
    if(k) cout<<-1;
    else cout<<ans;
    return 0;
}

UVA11475 Extend to Palindrome

::::info[题目大意]
给定一个字符串,要求在加最少的字母,使得原串变为一个回文串。
::::
原串可以拆成 AB 的形式,其中 B 是一个回文串。最优解一定是在后面加上 A 倒过来得到的字符串。那么,为了让添加的字符串的长度尽可能小,我们需要让 B 的长度尽可能大,也就是说我们需要找出原串的最长回文后缀。这可以用 Manacher 来做,我们枚举回文后缀的对称点即可。

这道题提交不了就不给代码了。

P6216 回文匹配

::::info[题目大意]
对于一对字符串 \((s_1,s_2)\),若 \(s_1\) 的长度为奇数的子串 \([l,r]\) 满足 \([l,r]\) 是回文的,那么 \(s_1\) 的“分数”会增加 \(s_2\)\([l,r]\) 中出现的次数。

现在给出一对 \((s_1,s_2)\),请计算出 \(s_1\) 的“分数”。
::::
好题啊!考虑计算出所有与 \(s_2\) 相同的子串对答案的贡献,所以我们可以用 Manacher 找出所有长度为奇数的回文串,然后差分记录每个区间出现的次数。
::::success[暴力代码]

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=3e6+5,mod=1ll<<32;
int slen,tlen,nxt[N],mr,mid,p[N],ans,sum[N];
string s,t;
signed main()
{
    cin>>slen>>tlen>>s>>t;
    s="^"+s+"%";
    t=" "+t;
    for(int i=1;i<=slen;i++)
    {
        if(i<=mr) p[i]=min(p[2*mid-i],mr-i+1);
        else p[i]=1;
        while(i-p[i]>=1&&i+p[i]<=slen&&s[i+p[i]]==s[i-p[i]]) p[i]++;
        if(i+p[i]-1>mr) mr=i+p[i]-1,mid=i;
        for(int j=(tlen+2)/2;j<=p[i];j++)
        {
            int lt=i-j+1,rt=i+j-tlen;
            sum[lt]++,sum[rt+1]--;
        }
    }
    for(int i=1;i<=slen;i++) sum[i]+=sum[i-1];
    for(int i=2,j=0;i<=tlen;i++)
    {
        while(j&&t[j+1]!=t[i]) j=nxt[j];
        if(t[j+1]==t[i]) j++;
        nxt[i]=j;
    }
    for(int i=1,j=0;i<=slen;i++)
    {
        while(j&&t[j+1]!=s[i]) j=nxt[j];
        if(t[j+1]==s[i]) j++;
        if(j==tlen)
        {
            ans=(ans+sum[i-tlen+1])%mod;
            j=nxt[j];
        }
    }
    cout<<ans;
    return 0;
}

::::
这坨代码的时间复杂度太高了,我们考虑优化。我们会发现每次修改都是对一个 \(sum\) 中的一个区间加 \(1\),另一个区间减 \(1\),所以我们可以直接再套一层差分求解 \(sum\)

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=3e6+5,mod=1ll<<32;
int slen,tlen,nxt[N],mr,mid,p[N],ans,sum[N];
string s,t;
signed main()
{
    cin>>slen>>tlen>>s>>t;
    s="^"+s+"%";
    t=" "+t;
    for(int i=1;i<=slen;i++)
    {
        if(i<=mr) p[i]=min(p[2*mid-i],mr-i+1);
        else p[i]=1;
        while(i-p[i]>=1&&i+p[i]<=slen&&s[i+p[i]]==s[i-p[i]]) p[i]++;
        if(i+p[i]-1>mr) mr=i+p[i]-1,mid=i;
        if((tlen+2)/2>p[i]) continue;
        int lt=i-p[i]+1,rt=i-(tlen+2)/2+1;
        if(lt<=rt) sum[lt]++,sum[rt+1]--;
        lt=i+(tlen+2)/2-tlen+1,rt=i+p[i]-tlen+1;
        if(lt<=rt) sum[lt]--,sum[rt+1]++;
    }
    for(int i=1;i<=slen;i++) sum[i]+=sum[i-1];
    for(int i=1;i<=slen;i++) sum[i]+=sum[i-1];
    for(int i=2,j=0;i<=tlen;i++)
    {
        while(j&&t[j+1]!=t[i]) j=nxt[j];
        if(t[j+1]==t[i]) j++;
        nxt[i]=j;
    }
    for(int i=1,j=0;i<=slen;i++)
    {
        while(j&&t[j+1]!=s[i]) j=nxt[j];
        if(t[j+1]==s[i]) j++;
        if(j==tlen)
        {
            ans=(ans+sum[i-tlen+1])%mod;
            j=nxt[j];
        }
    }
    cout<<ans;
    return 0;
}

P4555 [国家集训队] 最长双回文串

::::info[题目大意]
顺序和逆序读起来完全一样的串叫做回文串。比如 acbca 是回文串,而 abc 不是:abc 的顺序为 abc,逆序为 cba,不相同。

输入长度为 \(n\) 的串 \(S\),求 \(S\) 的最长双回文子串 \(T\),即可将 \(T\) 分为两部分 \(X, Y\)\(|X|,|Y|≥1\))且 \(X\)\(Y\) 都是回文串。
::::
首先看到回文串自然会想到 Manacher,然后可以考虑枚举两个回文串的分割点 \(i\),那么答案就是 \(rt_{i}+lt_{i+1}\),其中 \(lt_i\) 表示以 \(i\) 为左端点的最长回文串长度,\(rt_i\) 表示以 \(i\) 为右端点的最长回文串长度。

我们不难发现每一个位置 \(i\)\(lt_i\) 都是由最大的满足 \(j-p_j+1 \le i\) 的位置 \(j\) 转移过来的,所以我们可以直接枚举每一个转移点,然后用它来更新答案,这样均摊是 \(O(n)\) 的,计算 \(rt_i\) 也可以用类似的方法。

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5;
string s,t;
int n,mr,mid,p[N],lt[N],rt[N],minn,maxn,ans,c0[N],c1[N];
signed main()
{
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    cin>>t;
    int n=t.size();
    for(int i=0;i<n;i++)
    {
        s+=t[i];
        s+="#";
    }
    s="~#"+s+"%";
    int len=s.size()-2;
    for(int i=1;i<=len-1;i++)
    {
        if(i<=mr) p[i]=min(p[2*mid-i],mr-i+1);
        else p[i]=1;
        while(s[i+p[i]]==s[i-p[i]]) p[i]++;
        if(i+p[i]-1>=mr) mr=i+p[i]-1,mid=i;
        if(i==1) continue;
        if(s[i]!='#') c0[i/2-1]=p[i]/2;
        else c1[i/2-1]=p[i]/2;
    }
    minn=n;
    for(int i=n-1;i>=0;i--)
    {
        minn=min(minn,i+1);
        for(int j=minn-1;j>=max(min(i-c1[i]+1,i-c0[i]+1),0);j--)
        {
            if(i-c0[i]+1<=j) lt[j]=max(lt[j],(i-j)*2+1);
            if(i-c1[i]+1<=j) lt[j]=max(lt[j],2*(i-j)+2);
        }
        minn=min(minn,max((i-max(c1[i],c0[i])+1),0));
    }
    maxn=-1;
    for(int i=0;i<n;i++)
    {
        for(int j=maxn+1;j<=min(max(i+c0[i]-1,i+c1[i]),n-1);j++)
        {
            if(i+c0[i]-1>=j) rt[j]=max(rt[j],2*(j-i)+1);
            if(j!=i&&i+c1[i]>=j) rt[j]=max(rt[j],2*(j-i));
        }
        maxn=max(maxn,min(max(i+c0[i]-1,i+c1[i]),n-1));
    }
    for(int i=1;i<n;i++) ans=max(ans,rt[i-1]+lt[i]);
    cout<<ans;
    return 0;
}

P14363 [CSP-S 2025] 谐音替换

::::info[题目大意]
小 W 是一名喜欢语言学的算法竞赛选手。在语言学中,谐音替换是指将原有的字词替换为读音相同或相近的字词。小 W 发现,谐音替换的过程可以用字符串来进行描述。具体地,小 W 将谐音替换定义为以下字符串问题:

给定 \(n\) 个字符串二元组,第 \(i\) (\(1 \leq i \leq n\)) 个字符串二元组为 \((s_{i,1}, s_{i,2})\),满足 \(|s_{i,1}| = |s_{i,2}|\),其中 \(|s|\) 表示字符串 \(s\) 的长度。

对于字符串 \(s\),定义 \(s\)替换如下:

  • 对于 \(s\) 的某个子串 \(y\),若存在 \(1 \leq i \leq n\) 满足 \(y = s_{i,1}\),则将 \(y\) 替换为 \(y' = s_{i,2}\)。具体地,设 \(s = x + y + z\),其中 \(x\)\(z\) 可以为空,“+” 表示字符串拼接,则 \(s\) 的替换将得到字符串 \(s' = x + y' + z\)

小 W 提出了 \(q\) 个问题,第 \(j\) (\(1 \leq j \leq q\)) 个问题会给定两个不同的字符串 \(t_{j,1}, t_{j,2}\),她想知道有多少种字符串 \(t_{j,1}\) 的替换能够得到字符串 \(t_{j,2}\)。两种 \(s\) 的替换不同当且仅当子串 \(y\) 的位置不同或用于替换的二元组 \((s_{i,1}, s_{i,2})\) 不同,即 \(x, z\) 不同或 \(i\) 不同。你需要回答小 W 提出的所有问题。
::::
普通的 AC 自动机只能处理单个的字符串,无法处理题目中的替换方法,所以我们可以考虑改良一下:将一对谐音二元组逐位拆成若干个形如 (a,b) 的二元组,其中 a 是原串中的字符,b 是替换后的字符。然后建 AC 自动机暴力跳失配指针即可。
::::success[暴力代码]

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5;
int n,q,fail[N],tr[N][676],cnt,dep[N],sum[N];
void insert(string s,string t)
{
    int len=s.size(),cur=0;
    for(int i=0;i<len;i++)
    {
        if(!tr[cur][(s[i]-'a')*26+t[i]-'a']) tr[cur][(s[i]-'a')*26+t[i]-'a']=++cnt,dep[cnt]=dep[cur]+1;
        cur=tr[cur][(s[i]-'a')*26+t[i]-'a'];
    }
    sum[cur]++;
}
void build()
{
    queue<int> q;
    for(int i=0;i<676;i++)
    {
        if(tr[0][i])
        q.push(tr[0][i]);
    }
    while(q.size())
    {
        int u=q.front();
        q.pop();
        for(int i=0;i<676;i++)
        {
            if(tr[u][i])
            {
                fail[tr[u][i]]=tr[fail[u]][i];
                q.push(tr[u][i]);
            }
            else tr[u][i]=tr[fail[u]][i];
        }
    }
}
int query(string s,string t)
{
    if(s.size()!=t.size()) return 0;
    int len=s.size(),cur=0,res=0,lt=-1,rt=-1;
    for(int i=0;i<len;i++)
    {
        if(s[i]!=t[i])
        {
            if(lt==-1) lt=i;
            rt=max(rt,i);
        }
    }
    for(int i=0;i<len;i++)
    {
        cur=tr[cur][(s[i]-'a')*26+t[i]-'a'];
        int j=cur;
        if(i<rt) continue;
        while(j)
        {
            if(i-dep[j]+1<=lt&&i>=rt) res+=sum[j];
            j=fail[j];
        }
    }
    return res;
}
string s1[N],s2[N],t1,t2;
signed main()
{
    cin>>n>>q;
    for(int i=1;i<=n;i++)
    {
        cin>>s1[i]>>s2[i];
        insert(s1[i],s2[i]);
    }
    build();
    while(q--)
    {
        cin>>t1>>t2;
        cout<<query(t1,t2)<<'\n';
    }
    return 0;
}

::::
这个代码只能拿 \(50\) 分,瓶颈在于跳失配指针,我们可以用倍增来优化。

#include<bits/stdc++.h>
using namespace std;
const int N=5e6+1;
int n,q,fail[N],cnt,dep[N],sum[N],up[N][24],tot[N][24];
vector<int> son[N];
map<int,int> tr[N];
inline int get_nxt(int u,int c)
{
    while(u&&!tr[u].count(c)) u=fail[u];
    return tr[u][c]=((tr[u].count(c))? tr[u][c]:0);
}
void insert(string &s,string &t)
{
    int len=s.size(),cur=0;
    for(int i=0;i<len;i++)
    {
        int x=(s[i]-'a')*26+t[i]-'a';
        if(!tr[cur].count(x))
        {
            tr[cur][x]=++cnt;
            dep[cnt]=dep[cur]+1;
            son[cur].push_back(x);
        }
        cur=tr[cur][x];
    }
    sum[cur]++;
}
void build()
{
    queue<int> q;
    for(int i:son[0]) q.push(tr[0][i]);
    while(q.size())
    {
        int u=q.front();
        q.pop();
        for(int i:son[u])
        {
            fail[tr[u][i]]=get_nxt(fail[u],i);
            q.push(tr[u][i]);
        }
    }
    for(int i=0;i<=cnt;i++)
    {
        up[i][0]=fail[i];
        tot[i][0]=sum[fail[i]];
    }
    for(int j=1;j<=23;j++)
    {
        for(int i=1;i<=cnt;i++)
        {
            up[i][j]=up[up[i][j-1]][j-1];
            tot[i][j]=tot[i][j-1]+tot[up[i][j-1]][j-1];
        }
    }
}
int query(string &s,string &t)
{
    if(s.size()!=t.size()) return 0;
    int len=s.size(),cur=0,res=0,lt=-1,rt=-1;
    for(int i=0;i<len;i++)
    {
        if(s[i]!=t[i])
        {
            if(lt==-1) lt=i;
            rt=max(rt,i);
        }
    }
    for(int i=0;i<len;i++)
    {
        cur=get_nxt(cur,(s[i]-'a')*26+t[i]-'a');
        int pos=cur;
        if(i<rt) continue;
        if(i-dep[cur]+1<=lt)res+=sum[cur];
        else continue;
        for(int j=23;j>=0;j--)
        {
            if(up[pos][j]!=0&&dep[up[pos][j]]>=i+1-lt)
            {
                res+=tot[pos][j];
                pos=up[pos][j];
            }
        }
    }
    return res;
}
string read()
{
    char ch;
    string res="";
    while((ch=getchar())<'a'||ch>'z') continue;
    res+=ch;
    while((ch=getchar())>='a'&&ch<='z') res+=ch;
    return res;
}
inline int read_()
{
    int num=0,res=1;
    char ch=getchar();
    while(!isdigit(ch))
    {
        if(ch=='-') res=-1;
        ch=getchar();
    }
    while(isdigit(ch)) num=(num<<3)+(num<<1)+(ch^48),ch=getchar();
    return num*res; 
}
void print(int x)
{
    if(x<0) putchar('-'),x=-x;
    if(x<10) putchar(x+'0');
    else print(x/10),putchar(x%10+'0');
}
string s1,s2;
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    n=read_(),q=read_();
    for(int i=1;i<=n;i++)
    {
        s1=read(),s2=read();
        insert(s1,s2);
    }
    build();
    while(q--)
    {
        s1=read(),s2=read();
        print(query(s1,s2));
        putchar('\n');
    }
    return 0;
}
posted @ 2026-08-02 14:49  ryderyang  阅读(4)  评论(0)    收藏  举报