【题解】白兔的字符串 🐰🔄🐇

循环同构字符串问题解法

题目回顾 📜

题目链接: 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|)

posted @ 2025-08-10 14:39  开珥  阅读(11)  评论(0)    收藏  举报