2026ICPC字符串训练(I)

前言

2026 ICPC 训练题单,字符串题,主要涉及 回文串、KMP、字典树。

CF126B

题面

给定一个字符串 \(s\),求一个字符串 \(t\),满足:

  1. \(t\) 在 \(s\) 中出现至少 \(3\) 次;
  2. \(t\) 为 \(s\) 的前缀和后缀。

若不存在 \(t\), 则输出 Just a legend。

其中 \(|s| \leq 10^6\)

思路

思路 1

该做法为我初想法。
对 \(s\) 正反求两次 KMP,遍历每个位置,判断其所在 border 是否同时为反向 border。

思路 2

对 \(s\) 求 KMP,然后通过 j = nxt[j]-1 向前跳 border 链,通过桶记录是否存在即可。

思路 3

考虑思路 2 向前跳 border 链的过程,发现,只要第一步跳到的位置 \(nxt[i] \neq 0\),即存在 border 链,显然后续跳链一定不优。

实现

#include <iostream>

using namespace std;

const int N = 1e6+5;

string s;
int n, nxt[N], b[N];

int main() {
    cin >> s;
    n = s.length();
    for(int i = 1, j = 0; i < n; i ++) {
        j = nxt[i-1];
        while(j && s[i] != s[j]) j = nxt[j-1];
        j += (s[i] == s[j]);
        ++ b[nxt[i] = j];
    }
    if(b[nxt[n-1]] >= 2 && nxt[n-1] != 0) {
        cout << s.substr(0, nxt[n-1]) << "\n";
    }
    else if(nxt[nxt[n-1]-1] != 0) {
        cout << s.substr(0, nxt[nxt[n-1]-1]) << "\n";
    }
    else cout << "Just a legend\n";
    return 0;
}

CF471D

题面

给定两个数组 \(a_n, b_w\),求存在多少个位置 \(p\) 满足:

\[\exists k \in \mathbb{Z}, \forall 1 \leq i \leq w, a_{p+i-1} = b_{i} + k \]

其中 \(1 \leq n, w \leq 2 \times 10^5\)

思路

差分后即可转化为普通 KMP

实现

#include <iostream>

using namespace std;

const int N = 4e5+5;
const int inf = 1e9+5;

int n, w, a[N], nxt[N], u, v, ans;

int main() {
    cin >> n >> w;
    for(int i = 0; i < n; i ++) {
        cin >> u;
        if(i != 0) {
            a[i+w-1] = u - v;
        }
        v = u;
    }
    for(int i = 0; i < w; i ++) {
        cin >> u;
        if(i != 0) {
            a[i-1] = u - v;
        }
        v = u;
    }
    a[w-1] = inf;
    if(w == 1) {
        cout << n << "\n";
        return 0;
    }
    if(n < w) {
        cout << "0\n";
        return 0;
    }
    for(int i = 1, j = 0; i < n+w-1; i ++) {
        j = nxt[i-1];
        while(j && a[i] != a[j]) j = nxt[j-1];
        j += (a[i] == a[j]);
        nxt[i] = j;
        if(nxt[i] == w-1) {
            ++ans;
        }
    }
    cout << ans << "\n";
    return 0;
}

CF1705D

题面

多测
给定两个长度为 \(n\) 的 01 串 \(s, t\)。
你可以对 \(s\) 进行若干次操作(可以为 \(0\)):

  • 选择一个 \(2 \leq i \leq n-1\),满足 \(s_{i-1} \neq s_{i+1}\);
  • 反转 \(s_i\);

最小化使得 \(s\) 变为 \(t\) 的操作次数,若无法操作使得 \(s\) 变为 \(t\),输出 -1

\(1 \leq n \leq 2\times10^5\)

思路

对 \(s_i\) 进行差分,发现操作不会改变差分中 \(0, 1, -1\) 的数量与相对顺序,且一次操作会将 \(1\) 或 \(-1\) 移动一格。
判断 \(1, -1\) 的数量与位移即可。

异或差分也行。

实现

#include <iostream>

#define ll long long

using namespace std;

const int N =  2e5+5;

int tt;
int n;
int s[N], t[N];
ll ans;
string str;
char st, ed;

void solve() {
    cin >> n;
    cin >> str;
    ans = 0;
    st = str[0];
    ed = str[n-1];
    for(int i = 1; i < n; i ++) {
        s[i] = str[i] - str[i-1];
    }
    cin >> str;
    for(int i = 1; i < n; i ++) {
        t[i] = str[i] - str[i-1];
    }
    if(str[0] != st || str[n-1] != ed) {
        cout << "-1\n";
        return;
    }
    for(int i = 1, j = 1; i < n || j < n; i ++, j ++) {
        while(s[i] == 0 && i < n) i ++;
        while(t[j] == 0 && j < n) j ++;
        if(i == n || j == n) {
            if(i != j) {
                cout << "-1\n";
            }
            else {
                cout << ans << "\n";
            }
            return;
        }
        if(s[i] != t[j]) {
            cout << "-1\n";
            return;
        }
        ans += abs(i-j);
    }
    cout << ans << "\n";
}

int main() {
    cin >> tt;
    while(tt --) solve();
}

CF835D

题面

若一个字符串为回文串,则其为 \(1\)-回文串;

若一个字符串 \(t_{n}\) 满足:

  1. \(t[1, \dots, \lfloor n/2 \rfloor] = t[ \lfloor n+3/2 \rfloor, \dots, n]\);
  2. \(t[1, \dots, \lfloor n/2 \rfloor]\) 为 \(k-1\)-回文串。

则 \(t\) 为 \(k\)-回文串。

给定一个字符串 \(s\),对于 \(i \in \{1, \dots, |s|\}\),输出其 \(i\)-回文子串的个数。

其中 \(|s| \leq 5000\)

题解

本题我没有想出思路,看的题解的思路。

设 \(d[i][j]\) 表示 \(t[i, \dots, j]\) 的最大回文阶数。

然后我们可以使用朴素的 \(O(n^2)\) 判断回文子串的方法:枚举中心点并向外扩张,来进行 \(d[i][j]\) 的转移。

实现

/*
n <= 5000 可以考虑朴素的回文串算法
用 d[l][r] 记录 s[l...r] 的最高回文等级,所以 d[l][r] = d[l][mid] + 1
*/
#include <iostream>

using namespace std;

const int N = 5005;

int d[N][N], n, ans[N];
string s;

int main() {
    cin >> s;
    n = s.length();
    for(int i = 0; i < n; i ++) {
        d[i][i] = 1;
        for(int k = 0;; k ++) {
            if(i+k >= n || i-k <0) break;
            if(s[i+k] != s[i-k]) break;
            ans[d[i-k][i+k] = d[i-k][i-1] + 1] ++;
        }
        for(int k = 0;; k ++) {
            if(i+k >= n || i-k-1 < 0) break;
            if(s[i+k] != s[i-k-1]) break;
            ans[d[i-k-1][i+k] = d[i-k-1][i-1] + 1] ++;
        }
    }
    for(int i = n; i > 1; i --) {
        ans[i-1] += ans[i];
    }
    for(int i = 1; i <= n; i ++) {
        cout << ans[i] << " ";
    }
    cout << "\n";
    return 0;
}

CF1902E

题面

对于两个字符串 \(a, b\),定义 \(C(a, b)\) 如下:

  • 若 \(a\) 为空,则 \(C(a, b) = b\);
  • 若 \(b\) 为空,则 \(C(a, b) = a\);
  • 若 \(a_{|a|} = b_{1}\),则 \(C(a, b) = C(a[1, \dots, |a|-1], b[2, \dots, |b|])\);
  • 否则 \(C(a, b) = a + b\)。

给定 \(n\) 个字符串 \(s_1, \dots, s_n\),求 \(\sum_{i=1}^{n}\sum_{j=1}^{n} |C(s_i, s_j)|\)。

其中 \(1 \leq n, \sum|s_i|, \leq 10^6\)

思路

正难则反,我们考虑求 \(|s_i|+|s_j|-|C(s_i, s_j)|\)。

显然,这为 \(LCP(\text{reverse}(s_i), s_j)\),我们可以通过将 \(s_i\) 反插入 Trie 中,再正向查找 \(s_j\)。

这里正反可以互换,为方便实现采用正插反查。

实现


#include <iostream>
#include <algorithm>

#define ll long long

using namespace std;

const int N = 1e6+5;

int n;
ll ans;
string s[N];
int ch[N][26], cnt, siz[N];

void insert(string str, int len) {
    int now = 0, c;
    for(int i = 0; i < len; i ++) {
        c = str[i] - 'a';
        if(!ch[now][c]) {
            ch[now][c] = ++cnt;
        }
        now = ch[now][c];
        ++siz[now];
    }
}
void query(string str, int len) {
    int now = 0, c;
    for(int i = 0; i < len; i ++) {
        c = str[i] - 'a';
        if(!ch[now][c]) return;
        now = ch[now][c];
        ans -= 2*siz[now];
    }
}

int main() {
    cin >> n;
    for(int i = 1; i <= n; i ++) {
        cin >> s[i];
        insert(s[i], s[i].length());
        ans += s[i].length();
    }
    ans *= 2*n;
    for(int i = 1; i <= n; i ++) {
        reverse(s[i].begin(), s[i].end());
        query(s[i], s[i].length());
    }
    cout << ans << "\n";
    return 0;
}

CF432D

题面

给定一个字符串 \(s\),求其每一个 border 在其中出现的次数。

其中 \(|s| <= 10^5\)

思路

我 border 的 border 也是我的 border。

所以我们直接跳 border 链,后缀和求解。

实现

#include <iostream>
#include <stack>

using namespace std;

const int N = 1e5+5;

int n;
string s;
int nxt[N], siz[N];
stack<int> ans;

int main() {
    cin >> s;
    n = s.length();
    siz[0] = 1;
    for(int i = 1, j = 0; i < n; i ++) {
        siz[i] = 1;
        j = nxt[i-1];
        while(j && s[j] != s[i]) j = nxt[j-1];
        j += (s[i] == s[j]);
        nxt[i] = j;
    }
    for(int i = n-1; i > 0; i --) {
        if(nxt[i]>0) siz[nxt[i]-1] += siz[i];
    }
    int j = n-1;
    while(j>=0) {
        ans.push(j);
        j = nxt[j]-1;
    }
    cout << ans.size() << "\n";
    while(!ans.empty()) {
        cout << ans.top()+1 << " " << siz[ans.top()] << "\n";
        ans.pop();
    }
    return 0;
}

CF1200E*

题面

定义 \(\text{merge}(a, b) = a + b[len+1, \dots, |b|]\),

其中 \(len = \text{maxarg}_{l} a[|a|-l+1, \dots, |a|] = b[1, \dots, len]\)。

给定 \(n\) 个字符串 \(s_1, \dots, s_n\)。

记 \(t_1 = s_1, t_i = \text{merge}(t_{i-1}, s_i)\),求 \(t_n\)。

其中 \(\sum |s_i| \leq 10^6\)

题解

朴素的做法是对 \(s_i + \# +t_{i-1}\) 做一次 KMP 以求解 \(len\)。

但是这样复杂度是 \(O(n \times \sum|s_i|)\)。

注意到 \(len \leq |s_i|\),所以我们只需要 KMP 判断 \(t_{i-1}\) 的后 \(|s_i|\) 个字符,所以复杂度变为 \(O(\sum|s_i|)\)。

实现

#include <iostream>

using namespace std;

const int N = 2e6+5;

int nxt[N];

int kmp(const string& A, const string& B) {
    string s = B + "#" + (A.length() > B.length() ? A.substr( A.length()-B.length()) : A);
    int len = s.length();
    int j = 0;
    for(int i = 1; i < len; i ++) {
        j = nxt[i-1];
        while(j && s[i] != s[j]) j = nxt[j-1];
        j += (s[i] == s[j]);
        nxt[i] = j;
    }
    return j;
}

int n;
string A, B;

int main() {
    cin >> n;
    for(int i = 1; i <= n; i ++) {
        cin >> B;
        A += B.substr(kmp(A, B));
    }
    cout << A << "\n";
    return 0;
}

CF25E

题面

给定三个字符串 \(s_1, s_2, s_3\)

求最短的字符串 \(t\) 的长度,满足 \(s_1, s_2, s_3\) 均为 \(t\) 的子串。

其中 \(|s_i| \leq 10^5\)

思路

和上一题类似,我们考虑按照顺序合并 \(s_i\)。

首先对 \(s_i\) 进行全排列,然后按次合并:

  • 若 \(a\) 为 \(b\) 的子串,返回 \(b\);
  • 若 \(b\) 为 \(a\) 的子串,返回 \(a\);
  • 返回和上一题一样的 \(\text{merge}(a, b)\)。

复杂度为 \(O(n)\),常数大约有 \(30\)。

实现

#include <iostream>

using namespace std;

const int N = 3e5+50;

int ans = 1e9+5, nxt[N];
string s[3];

int kmp(const string& A, const string& B) {
    string s = B + '#' + A;
    int len = s.length(), j = 0;
    for(int i = 1; i < len; i ++) {
        j = nxt[i-1];
        while(j && s[i] != s[j]) j = nxt[j-1];
        if(s[i] == s[j]) {
            if(j == B.length()-1) return -1;
            j ++;
        }
        nxt[i] = j;
    }
    return j;
}

void check(int a, int b, int c) {
    string A = s[a];
    int l;
    l = kmp(A, s[b]);
    if(l != -1) A += s[b].substr(l);
    l = kmp(A, s[c]);
    if(l != -1) A += s[c].substr(l);
    ans = min(ans, (int)A.length());
}

int main() {
    cin >> s[0] >> s[1] >> s[2];
    check(0, 1, 2);
    check(0, 2, 1);
    check(1, 0, 2);
    check(1, 2, 0);
    check(2, 0, 1);
    check(2, 1, 0);
    cout << ans << "\n";
    return 0;
}

CF1537E2

题面

给定两个整数 \(n, k\),与一个长度为 \(n\) 的字符串 \(s\)。

你可以对 \(s\) 进行若干次以下两种操作:

  1. 删除 \(s\) 的最后一个元素;
  2. 令 \(s \gets s + s\)。

求最终的长度为 \(k\) 的字典序最小的字符串 \(s\)。

思路

思路 1

显然,答案一定是 \(s\) 的某个前缀 \(p\) 的无限重复。

那么若 \(p\) 为答案,则一定有 \(p <> s[|p|+1, \dots, 2|p|]\)。

首先对 \(s+s\) 进行 KMP,

然后我们枚举 \(p\),然后通过二分,找出 \(p\) 与 \(s[|p|+1, \dots, 2|p|]\) 的 LCP,可以通过比较 \(p[|LCP|]\) 与 \(s[|p|+|LCP|]\) 即可。

思路 2

我们通过双指针实现。

\(i\) 代表匹配的前缀 \(p\), \(j\) 代表被匹配的 \(s[|p|+1, \dots, 2|p|]\),显然:

  • \(s[i] = s[j]\),令二者加一;
  • \(s[i] > s[j]\),令 \(i \gets 1\),开始重新匹配;
  • \(s[i] < s[j]\),这时 \(p <> s[|p|, \dots, 2|p|]\),\(|p| = j\)。

实现

思路 1 实现详见 CF1537E2。

#include <iostream>

using namespace std;

int n, k;
string s;

int main() {
    cin >> n >> k >> s;
    int i = 0, j = 1, ans = 0;
    while(j < n) {
        if(s[i] == s[j]) {
            ++i;
            ++j;
        }
        else if(s[i] > s[j]) {
            ans = j;
            i = 0;
            ++j;
        }
        else break;
    }
    for(i = 0; i < k; i ++) {
        cout << s[i%(ans+1)];
    }
    cout << "\n";

    return 0;
}

CF808G

题面

给定两个字符串 \(s, t\)。

其中 \(s\) 仅包含小写字母和 ?,\(t\) 仅包含小写字母。

你需要将 \(s\) 中的所有 ?,替换成小写字母,并最大化 \(s\) 中 \(t\) 作为子串出现的次数。

其中 \(|s|, |t| \leq 10^5\), \(|s|\cdot|t| \leq 10^7\)

思路

数据范围提示我们,这题是 \(O(|s|\cdot|t|)\) 的。

首先,我们可以枚举,以求解 \(s\) 中可以安放 \(t\) 的位置。定义 \(\text{ok}[i]=1\) 代表 \(s[i, \dots, i+|t|-1] = t\)。

然后考虑 DP。

若 \(\text{ok}[i] = 1\),那么显然有 \(\displaystyle dp[i] = \max \left ( \max_{j \leq i - |t|} (dp[j]) , \max_{j = i - |t| + u, u \in \text{border}(t)} (dp[j])\right) + 1\)。

前者可以通过前缀最大值求解,后者通过 KMP 求解 border 链即可。

实现

#include <iostream>
#include <vector>

using namespace std;

const int N = 1e5+5;

int n, m;
string s, t;
int dp[N], mx[N], nxt[N];
bool ok[N];
vector<int> border;

void get_ok() {
    for(int i = 0; i < n; i ++) {
        for(int j = 0; j < m; j ++) {
            if(s[i+j] != t[j] && s[i+j] != '?') break;
            if(j == m-1) ok[i] = 1;
        }
    }
}

void kmp() {
    for(int i = 1, j = 0; i < m; i ++) {
        j = nxt[i-1];
        while(j && t[i] != t[j]) j = nxt[j-1];
        j += (t[i] == t[j]);
        nxt[i] = j;
    }
    for(int j = m-1; j>0;) {
        border.push_back(m-nxt[j]);
        j = nxt[j]-1;
    }
}

int main() {
    cin >> s >> t;
    n = s.length();
    m = t.length();
    if(m > n) {
        cout << "0\n";
        return 0;
    }
    kmp();
    get_ok();
    for(int i = 0; i < n; i ++) {
        if(ok[i]) {
            if(i-m >= 0) dp[i] = mx[i-m];
            for(auto u: border) {
                if(i-u >= 0) dp[i] = max(dp[i], dp[i-u]);
            }
            ++ dp[i];
        }
        mx[i] = max(mx[i-1], dp[i]);
    }
    cout << mx[n-1] << "\n";
    return 0;
}

CF149E

题面

给定一个字符串 \(s\) 与 \(m\) 个字符串 \(\{t_1, \dots, t_m\}\)。

你需要计算满足以下条件的 \(t_i\) 的数量:

  • 存在 \(a \leq b \lt c \leq d\),使得 \(s[a, \dots, b] + s[c, \dots, d] = t\)。

其中 \(|s| \leq 10^5, m \leq 100, |s_i| \leq 1000\)

思路

对于一个 \(t_i\) 我们枚举其长度为 \(j\) 的前缀第一次在 \(s\) 中出现时最后一个字符的位置 \(pre[i][j]\),

与其长度为 \(k\) 的后缀最后一次在 \(s\) 中出现时第一个字符距离 \(s\) 末尾的距离 \(suf[i][j]\)。

可以通过 KMP 在 \(O(m \dots |s|)\) 的时间求解。

然后我们只需要比较 \(pre[i][j] + suf[i][|t_i|-j]\) 与 \(|s|\) 即可。

枚举一下,复杂度 \(O(m \dots |t_i|)\)。

需要注意的是 \(s[a, \dots, b]\) 与 \(s[c, \dots , d]\) 均非空。

实现

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

const int N = 1e5+1e3+50;

int n, m, ans, nxt[N];
string s, t, p, sp;
vector<int> pos, sop;

void kmp(const string& str, int len, int l, bool tp) {
    int base = 0;
    for(int i = 1, j = 0; i < len; i ++) {
        j = nxt[i-1];
        while(j && str[i] != str[j]) j = nxt[j-1];
        j += (str[i] == str[j]);
        nxt[i] = j;
        if(i > l && nxt[i] > base) {
            if(tp) pos.push_back(i-l);
            else sop.push_back(i-l);
            ++ base;
        }
    }
}

void solve() {
    pos.clear();
    sop.clear();
    pos.push_back(0);
    sop.push_back(0);

    cin >> t;
    p = t + "#" + s;
    int l = t.length();
    kmp(p, p.length(), l, 1);
    reverse(t.begin(), t.end());
    p = t + "#" + sp;
    kmp(p, p.length(), l, 0);
    for(int i = 1; i < pos.size(); i ++) {
        if(l-i < sop.size() && l-i > 0) {
            if(pos[i]+sop[l-i] <= s.length()) {
                ++ans;
                return;
            }
        }
    }
}

int main() {
    cin >> s >> m;
    n = s.length();
    sp = s;
    reverse(sp.begin(), sp.end());
    while(m --) solve();
    cout << ans << "\n";
    return 0;
}

CF1202E

题面

定义 \(f(s, t)\) 为 \(t\) 作为子串在 \(s\) 中出现的次数。

给定一个字符串 \(t\) 与 \(n\) 个字符串 \(\{s_1, \dots, s_n\}\)。

求 \(\sum_i \sum_j f(t, s_i + s_j)\)。

其中 \(|t|, \sum|s_i| \leq 2 \times 10^5\)。

思路

定义 \(\text{end}[k]\) 代表 \(s_i\) 在 \(t\) 中出现,且以 \(k\) 为结尾的次数;
定义 \(\text{start}[k]\) 代表 \(s_i\) 在 \(t\) 中出现,且以 \(k\) 为开头的次数。

所以答案为 \(\sum_i \text{end}[i] \times \text{start}[i+1]\)。

通过 AC自动机 正序可以求出 \(\text{end}[]\),再对于反串建立 AC自动机即可。

实现

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

const int N = 1e5+1e3+50;

int n, m, ans, nxt[N];
string s, t, p, sp;
vector<int> pos, sop;

void kmp(const string& str, int len, int l, bool tp) {
    int base = 0;
    for(int i = 1, j = 0; i < len; i ++) {
        j = nxt[i-1];
        while(j && str[i] != str[j]) j = nxt[j-1];
        j += (str[i] == str[j]);
        nxt[i] = j;
        if(i > l && nxt[i] > base) {
            if(tp) pos.push_back(i-l);
            else sop.push_back(i-l);
            ++ base;
        }
    }
}

void solve() {
    pos.clear();
    sop.clear();
    pos.push_back(0);
    sop.push_back(0);

    cin >> t;
    p = t + "#" + s;
    int l = t.length();
    kmp(p, p.length(), l, 1);
    reverse(t.begin(), t.end());
    p = t + "#" + sp;
    kmp(p, p.length(), l, 0);
    for(int i = 1; i < pos.size(); i ++) {
        if(l-i < sop.size() && l-i > 0) {
            if(pos[i]+sop[l-i] <= s.length()) {
                ++ans;
                return;
            }
        }
    }
}

int main() {
    cin >> s >> m;
    n = s.length();
    sp = s;
    reverse(sp.begin(), sp.end());
    while(m --) solve();
    cout << ans << "\n";
    return 0;
}
posted @ 2026-09-16 13:59  Xie2Yue  阅读(12)  评论(0)    收藏  举报