【题解】白兔的字符串 🐰🔄🐇
循环同构字符串问题解法
题目回顾 📜
题目链接: https://ac.nowcoder.com/acm/problem/15253
给定一个字符串 T,定义两个字符串循环同构:如果把其中一个字符串首字符不断移动到末尾,经过若干次后能变成另一个字符串,就称它们循环同构。
现在给你 n 个字符串 S₁, S₂, ..., Sₙ,统计每个字符串中有多少长度为 |T| 的子串是和 T 循环同构的。
核心思想 💡
循环同构定义
对字符串 t 进行循环处理。
给定字符串 \(t = t_0 t_1 \ldots t_{m-1}\),对位置 \(i\) 的旋转定义为:
\[t^{(i)} = t_i t_{i+1} \ldots t_{m-1} t_0 t_1 \ldots t_{i-1}
\]
小例子直观理解 👀
假设 T="abab",则:
T + T = "abababab"
长度 m = 4
循环同构的字符串有:
"abab" (从位置0开始)
"baba" (从位置1开始)
"abab" (从位置2开始)
"baba" (从位置3开始)
滚动哈希加速匹配
使用多项式哈希快速计算子串哈希值,避免字符串逐个字符比较导致超时。
详细解法步骤 🛠️
1. 构造旋转哈希集合
方法一:拼接字符串 T = t + t
// 计算T的前缀哈希值
for (int i = 0; i < T.size(); i++) {
pT[i+1] = pT[i] * b + (T[i] - 'a' + 1);
}
// 寻找T中长度为t.size()的子串,枚举所有长度为 m 的子串哈希,存入集合kT
for (int i = 0; i < m; i++) {
ull res = pT[i+m] - pT[i] * v[m];
kT.push_back(res);
}
方法二:直接旋转字符串 t
预处理前缀哈希 \(H_t[i]\)(长度为 i 的前缀),和幂数组 \(b^k\) 方便计算。
现在要计算旋转后字符串的哈希 \(H(t^{(i)})\),它等于:
\[H(t^{(i)}) = \sum_{k=0}^{m-1} \bigl(t_{(i+k) \bmod m} - 'a' + 1\bigr) \times b^{m - 1 - k}
\]
这看起来复杂,但巧妙利用前缀哈希和幂的性质,可以转化为:
\[H(t^{(i)}) = \bigl(H_t[m] - H_t[i] \times b^{m - i}\bigr) \times b^{i} + H_t[i]
\]
// 先计算t的前缀哈希值
for (int i = 0; i < m; i++) {
pt[i+1] = pt[i] * b + (t[i] - 'a' + 1);
}
// 旋转t
for (int i = 0; i < m; i++) {
ll res = (pt[m] - pt[i] * v[m-i]) * v[i] + pt[i];
kt.insert(res);
}
2. 滚动哈希基础 🔍
- 选择基数 b(如131)
- 预处理幂数组,方便 O(1) 计算任意子串哈希
const ull b = 131;
vector<ull> v; // 定义前缀和和幂
void prem(ll n) {
v.resize(n + 1, 0);
v[0] = 1;
for (int i = 1; i <= n; i++) {
v[i] = v[i-1] * b;
}
}
- 利用哈希值唯一性(碰撞概率极小)实现快速比较
代码示例 💻
方法一:字符串拼接
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
typedef long long ll;
const ull b = 131;
vector<ull> p;
// 预处理幂
void prem(ull n) {
p.resize(n + 1);
p[0] = 1;
for (ull i = 1; i <= n; i++) {
p[i] = p[i-1] * b;
}
}
int main() {
string t;
cin >> t;
int n;
cin >> n;
ll m = t.size();
prem(2 * m + 10);
string T = t + t;
vector<ull> q1(2*m + 1, 0);
// 计算T前缀和哈希
for (int i = 0; i < 2*m; i++) {
q1[i+1] = q1[i] * b + (T[i] - 'a' + 1);
}
// 旋转T
unordered_set<ull> q2;
for (int i = 0; i < m; i++) {
ull res = q1[i+m] - q1[i] * p[m];
q2.insert(res);
}
while (n--) {
string s;
cin >> s;
int k = s.size();
vector<ull> pS(k + 1, 0);
// 计算s的前缀哈希
for (int i = 0; i < k; i++) {
pS[i+1] = pS[i] * b + (s[i] - 'a' + 1);
}
// 记录s的子串的哈希值
ll ans = 0;
for (int i = 0; i + m <= k; i++) {
ull kk = pS[i+m] - pS[i] * p[m];
if (q2.count(kk))
ans++;
}
cout << ans << endl;
}
return 0;
}
方法二:直接旋转
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
typedef long long ll;
const ull b = 131;
vector<ull> v; // 定义前缀和和幂
void prem(ll n) {
v.resize(n + 1, 0);
v[0] = 1;
for (int i = 1; i <= n; i++) {
v[i] = v[i-1] * b;
}
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
string t;
cin >> t;
ll m = t.size();
prem(m);
vector<ll> pT(2*m + 1, 0), pt(m + 1, 0);
unordered_set<ll> kt;
// 直接旋转t
// 先计算t的前缀哈希值
for (int i = 0; i < m; i++) {
pt[i+1] = pt[i] * b + (t[i] - 'a' + 1);
}
// 旋转t
for (int i = 0; i < m; i++) {
ll res = (pt[m] - pt[i] * v[m-i]) * v[i] + pt[i];
kt.insert(res);
}
ll n;
cin >> n;
while (n--) {
ll ans = 0;
string s;
cin >> s;
// 计算s的前缀哈希
vector<ll> ps(s.size() + 1, 0);
for (int i = 0; i < s.size(); i++) {
ps[i+1] = ps[i] * b + (s[i] - 'a' + 1);
}
// 记录s的子串的哈希值
for (int i = 0; i + m <= s.size(); i++) {
ull res = ps[i+m] - ps[i] * v[m];
if (kt.count(res)) {
ans++;
}
}
cout << ans << endl;
}
return 0;
}
总结 🎯
- 利用字符串拼接快速找到所有循环同构字符串
- 滚动哈希提升字符串匹配效率
- 哈希集合判断子串循环同构,保证效率与准确性
时间复杂度: O(|T| + Σ|Si|)
空间复杂度: O(|T|)

浙公网安备 33010602011771号