在长串 s1 里找模式串 s2 的所有出现位置,暴力做法是:把 s2 对到每个起点上,逐字符比。最坏 O(|s1|·|s2|)——s1 = s2 = 106 个 a 时是 1012 次比较,必然超时。KMP 把“失配之后从哪一位重新开始比”预先算进一个数组,于是 s1 的指针永不回退,总时间 O(|s1| + |s2|)。
| 任务 | 做法 | 复杂度 | 关键代码 |
|---|---|---|---|
| s2 是否出现在 s1(暴力) | 每个起点逐字符比 | O(|s1|·|s2|) | 双层 for |
| s2 在 s1 的所有位置 | KMP | O(|s1|+|s2|) | i 不回退,j = f[j] |
| 每个前缀的最长 border | 对自己做一次 KMP | O(|s2|) | f[i] = j |
| 最短循环节 | n − f[n] | O(n) | 一行减法 |
| 所有 border | 沿 f 链跳 | O(链长) | j = f[j] |
| 每个后缀与前缀的 LCP | Z 函数 | O(n) | 盒子 [l, r] |
先把这一节的 4 样新东西拆开。核心只有 5 行,但要把 5 行写对,得先知道每一行在维护什么。
| 组成 | 含义 / 格式要求 |
|---|---|
为什么用 1 基 | f[i] 的含义是“前 i 个字符”,下标天然从 1 数。用 0 基写要处处减一,border 题差一位就整题错。 |
cin >> (s + 1) | 读进以 s[1] 开头的位置,s[0] 空着不用。括号不能省:cin >> s + 1 会被解析成 (cin >> s) + 1。 |
strlen(s + 1) | 从 s[1] 数到 '\0',得到的就是串长。数组至少开 maxlen + 2(留 s[0] 与结尾 '\0')。 |
1e6 的串 | char s[1000005] 是 1 MB。全局数组在静态区,不占栈;写在 main 里是栈上 1 MB,容易爆栈。 |
多组数据 | 记得清 f 数组。f 是按长度用的,只要重新算一遍 f[1..m] 就够,不必 memset 全部。 |
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
char s[MAXN]; // s[1] 是第一个字符,s[0] 闲置
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> (s + 1); // 读到 s[1] 起始的位置
int n = strlen(s + 1); // 串长
cout << n << " " << s[1] << " " << s[n] << "\n";
return 0;
}
// 输入 abcde → 输出 5 a e1 基下循环写 for (int i = 1; i <= n; ++i),i 就是“第 i 个字符”,不需要在脑子里做 i−1 的换算。这是字符串算法最省心的写法。
数组开小了不一定报错:少 1 个字节时 '\0' 写到数组外,属于未定义行为,可能本地没事、交上去 RE。
| 组成 | 含义 / 格式要求 |
|---|---|
f[i] 是什么 | s 的前 i 个字符(记为 s[1..i])的最长 border 长度。border = 既是真前缀又是真后缀的子串。 |
f[1] = 0 | 单个字符没有非本身的 border。这是整条递推的起点,也是所有“回退到底”的终点。 |
j 的含义 | 进入第 i 轮时 j = f[i−1],并且 s[1..j] 与 s[i−j..i−1] 已经相等。这两段相等是“可以直接接着比”的全部依据。 |
if (s[i] == s[j + 1]) | 已经对齐的两段后面再接一个相同字符,border 就延长一位:f[i] = j + 1。 |
while (j && s[i] != s[j + 1]) | 失配就往短里找下一个候选。候选只能是 f[j]、f[f[j]]……一路跳到 0,跳到底再比 s[1]。 |
j 必须写在 for 的初始化里 | 写成 for (int i = 2; i <= m; ++i) { int j = 0; ... } 每轮归零,f 全变 0。 |
j 从哪来 | 循环开始时 j = 0 = f[1],之后每轮结束 j = f[i],正好是下一轮需要的值。 |
const int MAXN = 1000005;
int f[MAXN];
// s 从 s[1] 开始,长度 m
void buildFail(const char* s, int m) {
f[1] = 0; // 单个字符没有 border
for (int i = 2, j = 0; i <= m; ++i) { // j 在这里声明:跨轮保留
while (j && s[i] != s[j + 1]) j = f[j]; // 失配:跳到次长 border
if (s[i] == s[j + 1]) ++j; // 匹配:border 延长 1
f[i] = j;
}
}
// s = "aabaaab" → f[1..7] = 0 1 0 1 2 2 3f[i] 永远不会超过 f[i−1] + 1。因为 f[i] 的 border 去掉最后一位就是 s[1..i−1] 的 border,长度必然 ≤ f[i−1]。这条性质决定了 while 里“往短跳”是唯一的出路。
多测题里 f 数组的元素会被逐个覆盖(i 从 1 到 m 全写一遍),所以不需要额外清空。
| 组成 | 含义 / 格式要求 |
|---|---|
i | 扫 s1 的指针,从左到右走一遍,永不回退。这是 O(n) 的来源。 |
j | 当前已经匹配的字符数。它同时是 s2 的下标:下一个要比的是 s2[j+1]。 |
while (j && ...) | 与建 fail 时同一行代码。KMP 匹配就是“用 s2 的 fail 信息驱动 s1 的扫描”。 |
if (j == m) | 完整匹配一次,出现位置是 i − m + 1(1 基)。 |
j = f[j] | ★ 不能写 j = 0。s2 = aa、s1 = aaa 时,位置 1 和 2 都该输出。 |
总复杂度 | i 走 n 步;j 每步最多加 1,故 while 总次数 ≤ n,整体 O(n + m)。 |
for (int i = 1, j = 0; i <= n; ++i) {
while (j && s1[i] != s2[j + 1]) j = f[j]; // 失配:沿 fail 后退
if (s1[i] == s2[j + 1]) ++j; // 这一位对上了
if (j == m) { // 整串对上
cout << i - m + 1 << "\n"; // 起点(1 基)
j = f[j]; // 允许重叠,继续找
}
}s2[j + 1] 在 j = m 时会读到 s2[m+1] = '\0'——只有忘记在匹配后回退 j 才会发生。
整个算法不需要对 s1 做任何预处理,也不需要 s1 的 fail 数组。流式数据(边读边匹配)也能用。
| 组成 | 含义 / 格式要求 |
|---|---|
z[i] 是什么 | t[1..] 与 t[i..] 的最长公共前缀长度。z[1] = n(自己和自己)。 |
l, r | 当前“最靠右”的已知匹配段 [l, r]:t[l..r] 与 t[1..r−l+1] 相同。 |
i ≤ r 时先抄 | 盒子内的部分已经对齐,所以 z[i] 至少是 min(z[i−l+1], r−i+1)。 |
再暴力往外扩 | 抄完之后继续逐字符比。这一步看着像暴力,实际总代价是 O(n)。 |
为什么是 O(n) | r 只增不减,每次 while 成功都让 r 右移,而 r ≤ n。总扩大次数 ≤ n。 |
更新盒子 | i + z[i] − 1 > r 说明右边界推远了,换上新的 l、r。 |
const int MAXN = 2000005;
int z[MAXN];
void getZ(const char* t, int n) { // t 从 t[1] 开始
z[1] = n;
int l = 1, r = 0;
for (int i = 2; i <= n; ++i) {
z[i] = 0; // 清掉上一次运行的值
if (i <= r) z[i] = min(z[i - l + 1], r - i + 1); // 盒子内先抄
while (i + z[i] <= n && t[1 + z[i]] == t[i + z[i]]) ++z[i];
if (i + z[i] - 1 > r) { l = i; r = i + z[i] - 1; }
}
}
// t = "aaa" → z = 3 2 1min 里的 r − i + 1 是“别抄出盒子”。少了它会用到还没验证的位置,答案偏大。
Z 函数配 t = s2 + '#' + s1 就能做匹配:z[i] ≥ |s2| 的位置就是一个出现。分隔符必须在字符集之外。
给出两个字符串 s1、s2。若 s1 的区间 [l, r] 子串与 s2 完全相同,则称 s2 在 s1 中出现了,出现位置为 l。求 s2 在 s1 中所有出现的位置。
另外,对 s2 的每个前缀,求出它的最长 border 长度(border 指“不是 s 本身”的、既是 s 的前缀又是 s 的后缀的子串)。
输入:第一行字符串 s1,第二行字符串 s2。
输出:先每行一个整数,按从小到大输出所有出现位置;最后一行输出 |s2| 个整数,第 i 个是 s2 长度为 i 的前缀的最长 border 长度。
数据范围:1 ≤ |s1|, |s2| ≤ 106,只含大写英文字母。
暴力匹配的浪费在哪:设 s2 已经和 s1 对上了 j 个字符,第 j+1 个失配。暴力会把 s1 的起点右移一格、j 清零重来,把刚刚确认过的 j 个字符又比一遍。n = m = 106、s1 = s2 = 全是 a 时,浪费是 1012 级别的。
关键观察:失配那一刻,我们已知 s1[i−j..i−1] = s2[1..j]。新的对齐方式相当于问:s2 的前缀 s2[1..k] 能不能同时等于 s2[1..j] 的后缀?如果能,就让 k 个字符“自动对上”,从 k+1 继续比。
“既是 s2[1..j] 的前缀、又是它的后缀”就是 border 的定义。满足条件的 k 恰好是 j、f[j]、f[f[j]]……一条链。挑最大的那个 k,最省事,也不会漏解。所以失配后的动作是 j = f[j],而不是 j = 0。
于是“跳到哪”只由 s2 自己决定,跟 s1 无关。把它预先算出来:让 s2 和自己做一次同样的匹配,得到 f[1..m]。这一步 O(m)。
正式匹配时 i 只管往前走,j 按 fail 调整,总调整次数不超过 n,整题 O(n + m)。匹配成功后写 j = f[j](此时 j = m),因为“允许重叠”,下一次出现可能从当前位置往前一点点开始。
① 预处理:对 s2 做 KMP,得到 f[1..m]
f[1] = 0; j = 0
for i = 2..m:
while j > 0 且 s2[i] != s2[j+1]: j = f[j]
if s2[i] == s2[j+1]: j++
f[i] = j
② 匹配:扫一遍 s1
j = 0
for i = 1..n:
while j > 0 且 s1[i] != s2[j+1]: j = f[j]
if s1[i] == s2[j+1]: j++
if j == m: 输出 i-m+1; j = f[j]
③ 输出 f[1..m]看清楚一件事:② 和 ① 是同一段代码,只是把“自己”换成了别的串。所以 KMP 只要写对一次,另一处照抄即可。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
char s1[MAXN], s2[MAXN];
int f[MAXN]; // f[i]:s2 前 i 个字符的最长 border 长度
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> (s1 + 1) >> (s2 + 1); // 1 基下标
int n = strlen(s1 + 1), m = strlen(s2 + 1);
// ① 预处理:s2 匹配自己,求出 f[1..m]
f[1] = 0; // 单字符没有非本身的 border
for (int i = 2, j = 0; i <= m; ++i) {
while (j && s2[i] != s2[j + 1]) j = f[j]; // 失配:沿 border 链后退
if (s2[i] == s2[j + 1]) ++j; // 对上了:border 延长 1
f[i] = j;
}
// ② 匹配:i 扫 s1,j 记录已匹配长度
for (int i = 1, j = 0; i <= n; ++i) {
while (j && s1[i] != s2[j + 1]) j = f[j];
if (s1[i] == s2[j + 1]) ++j;
if (j == m) { // 出现一次
cout << i - m + 1 << "\n";
j = f[j]; // 允许重叠,退到最长 border 继续
}
}
// ③ 输出 f[1..m],空格分隔,最后一个后换行
for (int i = 1; i <= m; ++i) {
cout << f[i];
cout << (i == m ? "\n" : " ");
}
return 0;
}| 代码片段 | 含义与理由 |
|---|---|
cin >> (s1 + 1) >> (s2 + 1); | 两个串都从下标 1 开始存。括号必须有,否则 >> 的优先级低于 +,意思就变了。 |
int n = strlen(s1 + 1), m = strlen(s2 + 1); | 1e6 的串求长度是 O(n),只做一次。此后所有下标都用 1..n。 |
f[1] = 0; | 递推的起点。若忘了这一句,f[1] 保留旧值,第一轮 while 就会用错数字。 |
for (int i = 2, j = 0; i <= m; ++i) | j 写在初始化部分:它在轮与轮之间保留,正好承担“上一轮的 border 长度”。 |
while (j && s2[i] != s2[j + 1]) j = f[j]; | ★ 整题的核心。一次跳过一个候选,跳的步数是均摊 O(1) 的。 |
if (s2[i] == s2[j + 1]) ++j; | 注意比的是 s2[j+1] 而不是 s2[j]:j 是“已对齐的长度”,下一个待比的下标是 j+1。 |
f[i] = j; | 把这一轮的结果存下来。下一轮直接用,不用重算。 |
for (int i = 1, j = 0; i <= n; ++i) | 匹配阶段。i 从 1 走到 n,一次也不回退。 |
if (j == m) { cout << i - m + 1 ... | 起点 = 终点 − 长度 + 1。i 是 s1 里匹配段的最后一个字符。 |
j = f[j]; | ★ 写成 j = 0 会漏掉重叠的出现。f[m] 是 s2 的最长 border,也是下一次可能的重叠长度。 |
cout << (i == m ? "\n" : " "); | 最后一行 |s2| 个整数,行末换行、中间一个空格。格式错一样 WA。 |
这是全节唯一需要动脑的地方。设 f[i−1] = j,含义是 s[1..j] = s[i−j..i−1]。要算 f[i],只有三种情况。
“f[i] ≤ f[i−1] + 1”的理由:若 s[1..k] 是 s[1..i] 的 border,去掉最后一位,s[1..k−1] 就是 s[1..i−1] 的 border,所以 k−1 ≤ f[i−1],即 k ≤ f[i−1] + 1。这条性质说明“长度每轮最多加 1”,也说明失配时只可能往短里找。
所以 while (j && ...) j = f[j]; 不是“随便往回退一点”,而是沿着 all-border 链按从大到小枚举候选。链上的值恰好是 j, f[j], f[f[j]], …, 0,既不会漏,也不会重复。
| i | s[1..i] | 这一轮发生了什么 | f[i] |
|---|---|---|---|
| 1 | a | 起点 | 0 |
| 2 | aa | j=0,s[2]=s[1] → j=1 | 1 |
| 3 | aab | j=1,s[3]≠s[2] → j=f[1]=0;s[3]≠s[1] | 0 |
| 4 | aaba | j=0,s[4]=s[1] → j=1 | 1 |
| 5 | aabaa | j=1,s[5]=s[2] → j=2 | 2 |
| 6 | aabaaa | j=2,s[6]≠s[3] → j=f[2]=1;s[6]=s[2] → j=2 | 2 |
| 7 | aabaaab | j=2,s[7]=s[3] → j=3 | 3 |
第 6 行是重点:j 从 2 跳到 1 不是“减一”,而是跳到 f[2]。如果这里写成 j−−,就退化成 O(nm)。
| 代码片段 | 含义与理由 |
|---|---|
while (j && s[i] != s[j + 1]) j = f[j]; | 情况 2:沿 border 链找下一个候选。j 变成 f[j] 之后,s[1..j] 仍然等于 s[i−j..i−1],只是变短了,所以可以继续比 s[j+1]。 |
if (s[i] == s[j + 1]) ++j; | 情况 1:延长。注意此时若 j 曾跳到 0,比的是 s[1],也就是从最小的 border 重来。 |
f[i] = j; | 情况 3 的出口:j 已经是 0 且 s[1] ≠ s[i] 时不再进入 if,f[i] = 0。 |
fail 数组的第二大用途是求周期。一个串 s(长 n)的周期 p 指:对所有 i + p ≤ n 都有 s[i] = s[i+p]。等价说法是“s 有长度 n − p 的 border”。于是最小周期 = n − f[n]。
// 假设 s[1..n] 与 f[1..n] 已经算好
int p = n - f[n]; // 最小周期
// 完整循环节:n 能整除 p 时,s 由 n/p 个相同的块拼成
int blocks = (n % p == 0) ? n / p : 0; // 0 表示“不完整周期”
cout << p << " " << blocks << "\n";
// 全部 border 长度(从长到短):沿 f 链走
for (int k = f[n]; k > 0; k = f[k])
cout << k << " ";| 代码片段 | 含义与理由 |
|---|---|
int p = n - f[n]; | f[n] 是 s 的最长 border。去掉“最长 border 与前缀重合的那一段”,剩下的就是最小移动量 p。 |
n % p == 0 ? n / p : 0 | 整除才是“若干个完整块”。如 abababa 的 p = 2,但 7 % 2 ≠ 0,它只是“不完整周期”——末尾被截断了。 |
for (int k = f[n]; k > 0; k = f[k]) | border 链。f[k] < k 恒成立,所以循环一定终止;但链长最坏是 n−1(全是同一字符时),别在每个位置都遍历整条链。 |
为什么对应 | 若 p 是周期,则 s[1..n−p] = s[p+1..n],即长度 n−p 的前缀等于同长度后缀——正是 border。反之亦然。 |
Z 函数是 KMP 的“兄弟”:它直接给出“每个后缀与整个串前缀的最长公共前缀”。把模式串拼在主串前面、中间加一个不在字符集里的分隔符,一次 Z 就能得到所有匹配位置。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 2000005; // |s1| + |s2| + 1 再留余量
char t[MAXN];
int z[MAXN];
void getZ(int n) { // t 从 t[1] 开始
z[1] = n;
int l = 1, r = 0;
for (int i = 2; i <= n; ++i) {
z[i] = 0;
if (i <= r) z[i] = min(z[i - l + 1], r - i + 1); // 盒子内先抄
while (i + z[i] <= n && t[1 + z[i]] == t[i + z[i]]) ++z[i];
if (i + z[i] - 1 > r) { l = i; r = i + z[i] - 1; }
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
string a, b;
cin >> a >> b; // a 主串,b 模式串
int n = a.size(), m = b.size();
for (int i = 1; i <= m; ++i) t[i] = b[i - 1];
t[m + 1] = '#'; // 分隔符:不在字符集里,跨界的匹配立刻失败
for (int i = 1; i <= n; ++i) t[m + 1 + i] = a[i - 1];
int N = m + 1 + n;
getZ(N);
for (int i = m + 2; i <= N; ++i)
if (z[i] >= m) cout << i - m - 1 << "\n"; // 起点 = i - (m + 1)
return 0;
}| 代码片段 | 含义与理由 |
|---|---|
t = b + '#' + a | 分隔符保证“跨过分隔符的 LCP”不会虚增。于是 z[i] ≥ m 恰好等价于“b 在 a 的位置 i−(m+1) 出现”。 |
z[i] = min(z[i - l + 1], r - i + 1) | 盒子内 t[i−l+1..] 与 t[i..] 已经对齐,能抄 z[i−l+1] 这么多;但抄不能越过盒子右端,所以再取 r−i+1。 |
while (i + z[i] <= n && ...) | 从抄来的长度继续往外扩。边界条件 i + z[i] ≤ n 防越界。 |
if (i + z[i] - 1 > r) | 只有右端被推远才换盒子。r 单调不减,所以总扩大次数 ≤ n,整体 O(n)。 |
i - m - 1 | 1 基下 a 的第 pos 个字符在 t 中的下标是 m+1+pos,反过来 pos = i − (m+1)。 |
为什么还学 Z | 求“每个后缀与前缀的 LCP”Z 一步到位;求 border 链 fail 更直接。两个都写熟,按题目挑。 |
WA,样例就错、fail 全为 0。—— j 声明在循环体内(for (int i = 2; ...) { int j = 0; ... }),每轮归零,f 永远算不出非零值。
RE / 越界。—— 数组按 |s1| 开。1 基下标要额外留出 s[0] 和结尾 '\0',必须开 |s1| + 2 以上;写 char s1[1000000] 装 106 个字符一定爆。
WA,漏掉重叠的出现。—— 匹配成功后写 j = 0。s2 = aa、s1 = aaa 时应输出 1 和 2 两个位置,写 0 只出 1 个。正确写法是 j = f[j]。
TLE。—— 失配时写成 --j 或 j = f[j-1] 之类的“退一格”版本,退化成 O(nm)。106 的对 a 串能跑出 1012 次比较。
WA,答案整体差 1。—— 0 基写法与 1 基写法混用。网上有 nxt[i] 表示 s[0..i] 的 border 的版本,含义与本节 f[i] 差一位,两套不能混。
WA,格式错。—— P3375 要求每个位置单独一行,最后一行才是 fail 数组;不少模板题是位置空格分隔在一行。先读输出格式再写输出。
关掉页面,把 P3375 从空文件敲一遍并 AC。
改一改 ①:把 while (j && ...) j = f[j]; 换成 if (j && ...) --j;,输入 s1 为 106 个 a、s2 为 1000 个 a,用 time 看超时差多少。
改一改 ②:把 j = f[j] 改成 j = 0,输入 aaa 与 aa,位置从两个变成一个。
改一改 ③:用 n − f[n] 做周期题。输入 cabcabca 得 3;输入 abababa 得 2,且 7 % 2 ≠ 0——它是“不完整周期”。
改一改 ④:把 Z 函数的 min(z[i-l+1], r-i+1) 改成 z[i-l+1],对拍 P3375 的输出,找出第一个出错的位置。
两个循环形状相同:j 每轮最多 +1,while 里每次至少 −1。设 j 的总增量 Δ+、总减少量 Δ−。因为 j ≥ 0 恒成立,有 Δ− ≤ Δ+;而 ++j 每轮至多执行一次,故 Δ+ ≤ n。于是 while 的总次数 ≤ n,整个匹配 O(n)。这种“把代价记到未来的增量上”的证明叫势能分析,j 就是势能。
两个恒等式,所有 border 题都从它们出发:
border 链没有对数界:s = an 时链就是 n−1, n−2, …, 1,长度 n−1。所以不能靠“链短”来保证复杂度。KMP 的 O(n) 来自势能分析:整场扫描里 +1 至多 n 次,因此 −1 至多 n 次,与某一次跳多远无关。这也解释了为什么“沿链跳”本身很便宜,而在每个位置都重新遍历整条链(例如求所有前缀的全部 border)会变成 O(n2)。
完整周期与不完整周期:n % (n − f[n]) == 0 时 s 由 n/(n−f[n]) 个相同的块拼成;不整除时只是“不完整周期”,如 abababa 的最小周期是 2,但 7 不是 2 的倍数。题目问“循环节个数”还是“周期长度”,答案完全不同。
Fine–Wilf 定理:若 p、q 都是 s 的周期且 p + q − gcd(p, q) ≤ |s|,则 gcd(p, q) 也是 s 的周期。它说明“两个周期会互相挤压出更小的周期”,是很多周期结论的来源。
Z 与 fail 的分工:z[i] 回答“从 i 开始的后缀,与整串开头能对上多少位”;f[i] 回答“前 i 个字符这一段里,前缀与后缀最长能重合多少”。一个以“位置”为轴,一个以“长度”为轴。要“每个位置与前缀的匹配长度”用 Z 更直接;要“前缀的 border 链、周期”用 fail 更直接。
电台把一条信息 S′ 反复拼接后循环播放:S′S′S′……。你收到的是其中一段连续的字符序列 S,长度 L 保证不小于 |S′|。求 S′ 的最小可能长度。
形式化地说:求 S 的最短子串 S′,使得 S 本身是无限串 S′S′S′…… 的一个子串。
输入:第一行一个整数 L(1 ≤ L ≤ 106);第二行恰好 L 个小写字母,即序列 S。
输出:一行一个整数,表示 S′ 的最小长度。
“S 是 S′ 无限重复的一段”等价于:存在 p,使得对每个满足 i + p ≤ L 的 i 都有 S[i] = S[i+p]。这个 p 有个名字,叫周期。
周期的“补集”是 border:p 是周期,当且仅当 S 有长度 L − p 的 border。想让 p 最小,就要让 border 最长。
先手算样例 cabcabca:最长的、既是前缀又是后缀的真前缀有多长?把它从 L 里减掉,得到什么?再想想为什么不需要判断整除。
记 p 为 S 的一个周期:S[i] = S[i+p] 对所有合法 i 成立。那么 S 去掉最后 p 个字符得到的前缀,与去掉最前 p 个字符得到的后缀完全相同,即 S 有长度 L−p 的 border。反过来,有长度 b 的 border 就等价于有周期 L−b。
所以“最小周期” ⟺ “最长 border”。最长 border 就是 fail 数组的最后一项 f[L]。答案 = L − f[L]。
为什么不需要整除判断:题目允许最后一段 S′ 被截断。样例 cabcabca 的 p = 3,8 不是 3 的倍数,但它确实是 abcabcabcabc 的一段子串。若要求“完整循环节个数”,才需要额外判 L % p == 0。
复杂度:只需求一次 f[1..L],O(L) 时间、O(L) 空间,L = 106 轻松通过。
边界:L = 1 时 f[1] = 0,答案 1;S 全为同一字符时 f[L] = L−1,答案 1。两端都对。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
char s[MAXN];
int f[MAXN];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int L;
cin >> L;
cin >> (s + 1); // 1 基
f[1] = 0;
for (int i = 2, j = 0; i <= L; ++i) {
while (j && s[i] != s[j + 1]) j = f[j]; // 沿 border 链后退
if (s[i] == s[j + 1]) ++j;
f[i] = j;
}
// 最小周期 = 长度 - 最长 border
cout << L - f[L] << "\n";
return 0;
}| 片段 | 作用 |
|---|---|
cin >> (s + 1); | 题目先给长度 L 再给串,读到 s[1] 起始位置,后面所有下标都是 1 基,与 f 的定义对齐。 |
f[1] = 0; | 单字符没有非本身的 border。L = 1 时答案就是 1 − 0 = 1。 |
for (int i = 2, j = 0; i <= L; ++i) | j 写在初始化里,跨轮保留 f[i−1] 的值,这是递推能接上的关键。 |
while (j && s[i] != s[j + 1]) j = f[j]; | 失配跳 border 链。整段均摊 O(L),不会退化。 |
cout << L - f[L] << "\n"; | ★ 全部答案。最长 border 越长,周期越短;不判整除,因为末段允许被截断。 |
if (L % (L - f[L]) == 0) 才输出,样例之外的数据全错——本题允许不完整周期。char s[1000000]:1 基下标 + 结尾 '\0' 需要 106+2 个字节,会越界 RE。L - f[L - 1]:f 的下标是“前几个字符”,最后一项必须是 f[L]。cin >> s)却照抄 1 基的递推,整题位移一位。f[1] = 0 就是这条约束的起点。