题解:AtCoder AT_awc0047_e Repeat Playback and Partial Matching
【题目来源】
AtCoder:E - Repeat Playback and Partial Matching
【题目描述】
Takahashi has a song represented by a string \(S\) of length \(N\) consisting of lowercase English letters.
高桥有一首歌曲,用长度为 \(N\) 的字符串 \(S\) 表示,由小写英文字母组成。
Takahashi's music player has a repeat playback feature that automatically replays the song from the beginning when it ends. As a result, the string \(S\) infinitely concatenated with itself, \(S^{\infty} = SSS\ldots\), plays continuously. More precisely, the \(k\)-th character of \(S^{\infty}\) (\(k = 1, 2, 3, \ldots\)) is the \(((k - 1) \bmod N) + 1\)-th character of \(S\). Here, \(a \bmod b\) is the remainder when \(a\) is divided by \(b\), which is an integer at least \(0\) and less than \(b\).
高桥的音乐播放器具有重复播放功能,歌曲结束后会自动从头开始重播。因此,字符串 \(S\) 会无限地自我连接,形成 \(S^{\infty} = SSS\ldots\),持续播放。更准确地说,\(S^{\infty}\) 的第 \(k\) 个字符(\(k = 1, 2, 3, \ldots\))是 \(S\) 的第 \(((k - 1) \bmod N) + 1\) 个字符。这里,\(a \bmod b\) 是 \(a\) 除以 \(b\) 的余数,是一个不小于 \(0\) 且小于 \(b\) 的整数。
Aoki asks Takahashi \(Q\) questions. In the \(i\)-th question, two integers \(L_i\) and \(R_i\) (\(1 \leq L_i \leq R_i\)) are given. Let \(T_i\) be the contiguous substring of \(S^{\infty}\) from the \(L_i\)-th character to the \(R_i\)-th character. The length of \(T_i\) is \(R_i - L_i + 1\).
青木问了高桥 \(Q\) 个问题。在第 \(i\) 个问题中,给出两个整数 \(L_i\) 和 \(R_i\)(\(1 \leq L_i \leq R_i\))。设 \(T_i\) 为 \(S^{\infty}\) 中从第 \(L_i\) 个字符到第 \(R_i\) 个字符的连续子串。\(T_i\) 的长度为 \(R_i - L_i + 1\)。
For each question, determine whether \(T_i\) appears as a contiguous substring of the original string \(S\). That is, determine whether there exists an integer \(p\) satisfying \(1 \leq p\) and \(p + (R_i - L_i + 1) - 1 \leq N\) such that the contiguous substring of \(S\) from the \(p\)-th character to the \(p + (R_i - L_i + 1) - 1\)-th character matches \(T_i\).
对于每个问题,判断 \(T_i\) 是否作为原始字符串 \(S\) 的一个连续子串出现。也就是说,判断是否存在一个整数 \(p\) 满足 \(1 \leq p\) 且 \(p + (R_i - L_i + 1) - 1 \leq N\),使得 \(S\) 中从第 \(p\) 个字符到第 \(p + (R_i - L_i + 1) - 1\) 个字符的连续子串与 \(T_i\) 匹配。
Note that if the length \(R_i - L_i + 1\) of \(T_i\) exceeds \(N\), then \(T_i\) cannot be a contiguous substring of the string \(S\) of length \(N\), so the answer is always No.
注意,如果 \(T_i\) 的长度 \(R_i - L_i + 1\) 超过了 \(N\),那么 \(T_i\) 不可能是长度为 \(N\) 的字符串 \(S\) 的连续子串,因此答案始终是 No。
【输入】
\(N\) \(Q\)
\(S\)
\(L_1\) \(R_1\)
\(L_2\) \(R_2\)
\(\vdots\)
\(L_Q\) \(R_Q\)
- The first line contains an integer \(N\) representing the length of the string and an integer \(Q\) representing the number of questions, separated by a space.
- The second line contains the string \(S\) of length \(N\) representing the song.
- The following \(Q\) lines contain each question.
- The \((2 + i)\)-th line (\(1 \leq i \leq Q\)) contains the integers \(L_i\) and \(R_i\) representing the \(i\)-th question, separated by a space.
【输出】
Output \(Q\) lines. On the \(i\)-th line, output Yes if \(T_i\) appears as a contiguous substring of \(S\), and No otherwise.
【输入样例】
5 5
abcab
1 3
4 7
5 6
3 7
11 13
【输出样例】
Yes
No
No
No
Yes
【核心思想】
-
问题分析:给定长度为 \(N\) 的字符串 \(S\) 及其无限循环版本 \(S^{\infty} = SSS\ldots\),\(Q\) 次查询每次给出区间 \([L_i, R_i]\),判断 \(S^{\infty}\) 中对应的子串 \(T_i\) 是否也是原始字符串 \(S\) 的连续子串。关键在于高效处理跨越 \(S\) 边界的循环子串,并通过预处理实现每次查询 \(O(1)\) 回答。这是一个扩展 KMP(Z 函数)问题。
-
算法选择:
- Z 函数(扩展 KMP):\(z[i]\) 表示后缀 \(S[i..N]\) 与 \(S\) 前缀的最长公共前缀(LCP)长度,可在 \(O(N)\) 内预处理
- 反转串 Z 函数:构建 \(S\) 的反转串 \(S_{\text{rev}}\) 并计算其 Z 数组,将"以某位置结尾的后缀与前缀匹配"转化为"前缀与前缀匹配"
- 离线预处理 M 数组:枚举所有分割位置 \(k\),记录当后缀匹配长度至少为某值时,前缀匹配的最大长度,再通过后缀最大值处理实现 \(O(1)\) 查询
-
关键步骤:
- 读取输入:\(N\)、\(Q\)、字符串 \(S\)
- 计算 Z 数组:\(z[i]\) 表示从位置 \(i\) 开始的后缀与 \(S\) 前缀的 LCP 长度
- 构建反转串:\(S_{\text{rev}}[i] = S[N - i + 1]\),计算其 Z 数组 \(z_{\text{rev}}\)
- 预处理 M 数组:
- 遍历分割位置 \(k\) 从 \(1\) 到 \(N - 1\):
- \(l_{1_\max} = z_{\text{rev}}[N - k + 1]\):以位置 \(k\) 结尾的后缀与 \(S\) 前缀匹配的最大长度
- \(M[l_{1_\max}] = \max(M[l_{1_\max}],\; z[k + 1])\):记录对应的前缀匹配最大长度
- 后缀最大值处理:遍历 \(i\) 从 \(N\) 到 \(1\),\(M[i] = \max(M[i],\; M[i + 1])\)。含义:若后缀匹配长度 \(\geq i\) 时可匹配前缀长度 \(M[i]\),则匹配长度 \(\geq i - 1\) 时也可至少匹配这么多
- 遍历分割位置 \(k\) 从 \(1\) 到 \(N - 1\):
- 处理每个查询 \((L, R)\):
- \(\text{len} = R - L + 1\),若 \(\text{len} > N\) 则输出
No - \(\text{idx} = (L - 1) \bmod N + 1\)(在单个 \(S\) 副本中的起始位置)
- 情况 1(不跨越边界):若 \(\text{idx} + \text{len} - 1 \leq N\),子串完全在一个 \(S\) 副本内部,输出
Yes - 情况 2(跨越边界):
- \(L_1 = N - \text{idx} + 1\)(第一部分:从 \(\text{idx}\) 到末尾的后缀长度)
- \(L_2 = \text{len} - L_1\)(第二部分:从开头需要的前缀长度)
- 若 \(M[L_1] \geq L_2\):存在某个分割位置使得后缀匹配 \(\geq L_1\) 且前缀匹配 \(\geq L_2\),输出
Yes;否则No
- \(\text{len} = R - L + 1\),若 \(\text{len} > N\) 则输出
-
时间/空间复杂度:
- 时间复杂度:\(O(N + Q)\)。预处理两个 Z 数组 \(O(N)\),预处理 M 数组 \(O(N)\),每次查询 \(O(1)\)
- 空间复杂度:\(O(N)\),字符串、Z 数组和 M 数组
-
扩展 KMP / Z 函数的核心思想:
- 最长公共前缀(LCP):\(z[i]\) 快速给出后缀 \(S[i..N]\) 与整个字符串前缀的匹配长度,避免逐位比较
- 线性预处理:利用已计算的 Z 值和当前匹配区间 \([l, r]\),通过 \(z[i - l + 1]\) 的复用避免重复比较,实现 \(O(N)\) 复杂度
- 反转转化技巧:将后缀匹配问题通过字符串反转转化为前缀匹配问题,统一用 Z 函数处理前后缀关系
- 离线查询预处理:将动态查询条件(后缀长度 \(L_1\)、前缀长度 \(L_2\))通过 M 数组离线转化为"是否存在"的判定问题,实现查询 \(O(1)\)
- 适用于循环字符串匹配、前后缀公共部分分析、子串周期性问题等场景
【算法标签】
扩展KMP
【代码详解】
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 500005;
int n, q; // n: 字符串长度, q: 查询次数
char s[N], s_rev[N]; // s: 原始字符串, s_rev: 反转后的字符串
int z[N], z_rev[N]; // z: 原始字符串的Z数组, z_rev: 反转字符串的Z数组
int M[N]; // M[x]: 当后缀匹配长度至少为x时,前缀匹配的最大长度
// Z函数计算
void get_z(char *s, int z[])
{
z[1] = n; // z[1]定义为字符串长度
for (int i = 2, l, r = 0; i <= n; i++)
{
// 如果i在当前已知匹配区间内
if (i <= r)
{
z[i] = min(z[i - l + 1], r - i + 1);
}
// 扩展匹配
while (s[1 + z[i]] == s[i + z[i]])
{
z[i]++;
}
// 更新最右匹配边界
if (i + z[i] - 1 > r)
{
l = i;
r = i + z[i] - 1;
}
}
}
signed main()
{
cin >> n >> q;
// 读取字符串,注意从下标1开始
scanf("%s", s + 1);
// 计算原始字符串的Z数组
get_z(s, z);
// 创建反转字符串
for (int i = 1; i <= n; i++)
{
s_rev[i] = s[n - i + 1];
}
// 计算反转字符串的Z数组
get_z(s_rev, z_rev);
// 预处理M数组
// 对于每个分割位置k,记录当后缀匹配至少为某个长度时,前缀匹配的最大长度
for (int k = 1; k < n; k++)
{
int l1_max = z_rev[n - k + 1]; // 从位置k开始向前的最大匹配长度
M[l1_max] = max(M[l1_max], z[k + 1]); // 从位置k+1开始向后的最大匹配长度
}
// 后缀最大值处理
// 如果后缀匹配长度≥x时可以,那么匹配长度≥x-1时也可以
for (int i = n; i >= 1; i--)
{
M[i] = max(M[i], M[i + 1]);
}
// 处理每个查询
while (q--)
{
int L, R;
cin >> L >> R;
int len = R - L + 1; // 查询子串长度
if (len > n) // 子串长度超过原始字符串长度,不可能存在
{
cout << "No\n";
continue;
}
// 在S中的起始位置(1-indexed)
int idx = (L - 1) % n + 1;
// 情况1:子串不跨越字符串边界
if (idx + len - 1 <= n)
{
// 整个子串都在一个原始字符串副本内部
cout << "Yes\n";
}
else // 情况2:子串跨越字符串边界
{
// 分割为两部分:
int L1 = n - idx + 1; // 第一部分长度:从idx到字符串末尾
int L2 = len - L1; // 第二部分长度:从字符串开头到剩余部分
// 检查是否存在位置k使得:
// 1. 后缀匹配长度 ≥ L1
// 2. 前缀匹配长度 ≥ L2
if (M[L1] >= L2)
{
cout << "Yes\n";
}
else
{
cout << "No\n";
}
}
}
return 0;
}
【运行结果】
5 5
abcab
1 3
Yes
4 7
No
5 6
No
3 7
No
11 13
Yes
浙公网安备 33010602011771号