前言

在此谢罪:虽然这场考试后我干了一些无耻的事情,我认为主凶是数组大小中将 L 写成了 N(我是个伞兵)。

完善情况:

A B C D E F

P4391 [BOI2009]Radio Transmission 无线传输

给你一个长度为 \(L\) 的字符串 \(s_1\),它是由某个字符串 \(s_2\) 不断自我连接形成的。但是字符串 \(s_2\) 是不确定的,现在只想知道它的最短长度是多少。
对于全部的测试点,保证 \(1 < L \le 10^6\)

显然,答案为 \(L-\operatorname{fail}(L)\)。其中 \(\operatorname{fail}\)\(s_1\) 的失配函数。

给个简单的证明:

\(K=|s_2|\)。则 \(\operatorname{fail}(K)=0\)(如果不为 \(0\),那么一定有更小的)。

然后就有 \(\operatorname{fail}(K+n)=n(0\leq n\leq L-K)\)

所以 \(\operatorname{fail}(L)=L-K\)

所以 \(K=L-\operatorname{fail}(L)\)。证毕。

代码:

#include <bits/stdc++.h>
#define int long long
using namespace std;

const int N = 1e6+5;
int fail[N],n;
string s;

signed main(){
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    cin>>n>>s;
    s=" "+s;
    for(int i=2,j=0;i<=n;i++){
        while(j&&s[i]!=s[j+1]) j=fail[j];
        if(s[i]==s[j+1]) j++;
        fail[i]=j;
    }
    cout<<(n-fail[n]);
    return 0;
}

P4824 [USACO15FEB] Censoring S

给出两个字符串 \(S,T\)。你需要进行以上操作:

  • 删除一个 \(S\) 的,等于 \(T\) 的子串。
  • 重复以上操作,直到无法执行上面的操作。

输出最终的 \(S\)。保证答案不会是空串。
\(1 \leq |T| \lt |S| \leq 10^6\)

这道题可以使用 P3121 [USACO15FEB]Censoring G 的栈 + AC 自动机轻松解决(这道题是考试题的加强版)。不过也可以使用与其思想类似的 KMP。

先对 \(t\) 求一遍 \(\operatorname{fail}\)。然后进行匹配。如果匹配到了,说明需要删去。维护一个栈和匹配到的最大的位置 \(\operatorname{pos}\)。需要删去时弹出栈内 \(|t|\) 个元素,匹配指针 \(j\) 也退回以前的 \(\operatorname{pos}\)。这样就可以实现可删除 KMP 了。

时间复杂度不会算,不过大概是 \(O(|s|+|t|)\) 的样子。

#include <bits/stdc++.h>
#define int long long
using namespace std;

string s,t;
int n,m,fail[1000005],pos[1000005],sta[1000005],top;

signed main(){
    cin>>s>>t;
    n=s.length();m=t.length();s=" "+s;t=" "+t;
    for(int i=2,j=0;i<=m;i++){
        while(j&&t[i]!=t[j+1]) j=fail[j];
        if(t[i]==t[j+1]) j++;
        fail[i]=j;
    }
    for(int i=1,j=0;i<=n;i++){
        while(j&&s[i]!=t[j+1]) j=fail[j];
        if(s[i]==t[j+1]) j++;
        pos[i]=j;sta[++top]=i;
        if(j==m){
            top-=m;
            j=pos[sta[top]];
        }
    }
    for(int i=1;i<=top;i++) cout<<s[sta[i]];
    return 0;
}

P4503 [CTSC2014] 企鹅 QQ

定义:如果两个字符串 \(a,b\) 相似,当且仅当 \(|a|=|b|\),且有且仅有一个 \(k(1 \leq k \leq |a|)\)\(a_k\neq b_k\)
给出 \(n\) 个长度为 \(L\) 的,两两不同字符串 \(s_i\)。求出有多少个无序数对 \((i,j)\),使得 \(a_i,a_j\) 相似。
\(1 \leq n \leq 3\times10^4,1 \leq L \leq 200\)

首先我们考虑如何高效判断两个字符串相似,暴力是 \(O(L)\) 的。而且几乎无法优化。

我们考虑另一种思路,相似的两个字符串一定只有一个字符不同,我们枚举这个字符,然后就转换为了匹配字符前的部分和字符后的部分。容易看出字符前的部分就是前缀,字符后的部分就是后缀。预处理前缀和后缀 Hash 就可以实现高效匹配了。时间复杂度预处理 \(O(nL)\),单次 \(O(L)\)

沿着这个思路继续思考,我们可以枚举不同的位置,把所以字符串中以这个位置分隔的前缀 Hash 和后缀 Hash 拉出来。就变成了求一个序列中有多少对相同的元素。

这是一个经典问题,可以使用排序 + 双指针 + 简单的排列组合解决,时间复杂度 \(O(n\log n)\)

时间复杂度 \(O(nL\log n)\)。可以通过本题。

#include <bits/stdc++.h>
#define int long long
using namespace std;

typedef unsigned long long ull;
const int N = 30005,L = 205;
const ull BASE = 125;
int n,l,s,ans;
ull pre[N][L],suf[N][L];

struct node{
    ull pre,suf;
    bool operator<(const node &x) const {
        return (pre!=x.pre)?(pre<x.pre):(suf<x.suf);
    }
    bool operator==(const node &x) const {
        return pre==x.pre && suf==x.suf;
    }
} buf[N];

signed main(){
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    cin>>n>>l>>s;
    for(int i=1;i<=n;i++){
        string str;
        cin>>str;
        str=" "+str;
        for(int j=1;j<=l;j++) pre[i][j]=pre[i][j-1]*BASE+str[j];
        for(int j=l;j;j--) suf[i][j]=suf[i][j+1]*BASE+str[j];
    }
    for(int diff=1;diff<=l;diff++){
        for(int j=1;j<=n;j++){
            buf[j].pre=pre[j][diff-1];buf[j].suf=suf[j][diff+1];
        }
        sort(buf+1,buf+n+1);
        int l=1,r=1;
        while(l<=n){
            while(r<=n&&buf[l]==buf[r]) r++;
            r--;
            ans+=(((r-l+1)*(r-l))>>1);
            l=r+1;r=l;
        }
    }
    cout<<ans;
    return 0;
}

P7469 [NOI Online 2021 提高组] 积木小赛

给出两个长度为 \(n\) 的字符串 \(s,t\)。求出有多少个 \(t\) 的非空子串 \(a\),使得 \(a\)\(s\) 的子序列。
\(1 \leq n \leq 3\times10^3\)

首先枚举 \(t\) 的所有非空子串,固定左端点 \(l\),使右端点 \(r\) 递增。这时候如果存在子序列,那么子序列一定可以是一个前缀扩展形式(指前一个字符串是后一个字符串的前缀)。

这时我们可以维护一个指针 \(c\) 往右扫。贪心地找到最先发现的 \(s_c=t_r\)。贪心选前面的一定比选后面的不劣(选前面的选择性更多)。

最后考虑如何判重。我们可以计算出所有满足条件的字符串哈希值。然后丢到 unordered_setset,或随便搞一个数组(之后进行 sortunique)里面去。实测只有最后一个方法可以通过。

请使用 C++ 98。否则会喜提 \(90\) 分。(如果开 O2 就随意了)

时间复杂度 \(O(n^2\log n)\)。空间复杂度 \(O(n^2)\)

#include <bits/stdc++.h>
using namespace std;

typedef unsigned long long ull;
int n;
char s[3005],t[3005];
const ull BASE = 29;
ull sset[(3005*3005)],tot;

signed main(){
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    cin>>n;
    cin>>(s+1)>>(t+1);
    for(int i=1;i<=n;i++){
        int cur=1;
        ull hashing = 0;
        for(int j=i;j<=n;j++){
            while(cur<=n&&s[cur]!=t[j]) cur++; 
            if(cur>n) break;
            hashing=(hashing*BASE)+(t[j]-'a'+1);
            sset[++tot]=hashing;
            cur++;
        }
    }
    sort(sset+1,sset+tot+1);
    cout<<((unique(sset+1,sset+tot+1))-sset-1)<<'\n';
    return 0;
}

UVA11732 "strcmp()" Anyone?

\(T\) 组数据,每组数据给出 \(n\) 个字符串 \(S_i\)。对于每组数据,询问 \(\sum_{i=1}^n\sum_{j=i+1}^n cnt(\operatorname{strcmp}(i,j))\) 的值。其中 \(cnt(\operatorname{strcmp}(i,j))\) 表示对 \(S_i,S_j\) 执行一次 \(\operatorname{strcmp}\) 函数需要的比较次数,\(\operatorname{strcmp}\) 的实现如下:

int strcmp(char *s, char *t)
{
   int i;
   for (i=0; s[i]==t[i]; i++)
       if (s[i]=='\0')
           return 0;
   return s[i] - t[i];
}

\(1\leq T\leq 10\)\(1\le n\le4\times10^3\)\(1\le |S|\le10^3\)\(\Sigma=\{\mathtt{a\sim z},\mathtt{A\sim Z},\mathtt{0\sim 9}\}\),其中 \(\Sigma\) 表示可能在 \(S\) 中出现的字符集合

UVA1401 Remember the Word

多组数据,每组数据给出一个字符串 \(a\) 和一个长度为 \(s\) 的字典。你需要将 \(a\) 分割成几段,使得每一段都在字典中出现过。求方案数。对 \(20071027\) 取模。
\(1 \leq s \leq 4000,1 \leq |a| \leq 3\times 10^5\),字典中的单词长度不超过 \(100\),且字典中的单词互不相同。本题中的字符串仅包含英文小写字母。