2026ICPC字符串训练(I)
前言
2026 ICPC 训练题单,字符串题,主要涉及 回文串、KMP、字典树。
CF126B
题面
给定一个字符串 \(s\),求一个字符串 \(t\),满足:
- \(t\) 在 \(s\) 中出现至少 \(3\) 次;
- \(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\) 满足:
其中 \(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}\) 满足:
- \(t[1, \dots, \lfloor n/2 \rfloor] = t[ \lfloor n+3/2 \rfloor, \dots, n]\);
- \(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\) 进行若干次以下两种操作:
- 删除 \(s\) 的最后一个元素;
- 令 \(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;
}

浙公网安备 33010602011771号