[SDOI2015] 双旋转字符串

GitHub跳转原题关系图返回列表

按 N 与 M 的大小分类,把条件改写成在前半串循环串中匹配一段前后缀,再用 KMP 与哈希统计可行配对。

OJ: luogu

题目 ID: P3318

难度:提高+/省选-

标签:字符串KMP哈希建模分类讨论

日期: 2026-06-21 14:34

题意

给定两个字符串集合 ST

  • S 中每个字符串长度都是 N
  • T 中每个字符串长度都是 M
  • N + M 一定是偶数

要求统计有多少对 (S_i, T_j),使得拼接串 S_i + T_j 的前一半和后一半长度相同,并且后一半可以由前一半旋转得到。

思路

先看一个可以直接验证想法的朴素解:

cpp
#include <bits/stdc++.h>
using namespace std;

int total_s, total_t, n, m;
vector<string> s_list, t_list;

bool is_rotation(const string &a, const string &b) {
    if (a.size() != b.size()) {
        return false;
    }
    string t = a + a;
    return t.find(b) != string::npos;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> total_s >> total_t >> n >> m;
    s_list.resize(total_s);
    t_list.resize(total_t);
    for (int i = 0; i < total_s; i++) {
        cin >> s_list[i];
    }
    for (int i = 0; i < total_t; i++) {
        cin >> t_list[i];
    }

    int half = (n + m) / 2;
    long long answer = 0;

    // brute.cpp:直接枚举每一对 (S_i, T_j),检查拼接后前半段与后半段是否为旋转关系。
    for (int i = 0; i < total_s; i++) {
        for (int j = 0; j < total_t; j++) {
            string w = s_list[i] + t_list[j];
            string u = w.substr(0, half);
            string v = w.substr(half, half);
            if (is_rotation(u, v)) {
                answer++;
            }
        }
    }

    cout << answer << '\n';

    return 0;
}

暴力做法就是枚举每一对 (S_i,T_j),拼起来后把前半段和后半段取出,再判断它们是否互为旋转。这个方法复杂度太高,只适合很小的数据。

half = (N + M) / 2

核心在于按 NM 的大小关系分情况建模。

情况一:N >= M

此时把 S_i 记成:

  • P:前 half 个字符
  • Q:后 N-half 个字符

拼接后前半段就是 P
后半段则是 Q + T_j

要满足双旋转性,就要求:

Q + T_jP 的某个旋转。

这等价于:

  • 在循环串 P + P 中找一个起点
  • 从这个起点开始长度 N-half 的那一段必须等于 Q
  • 紧随其后的长度 M 的那一段就是候选 T_j

所以对每个 S_i,我们只需要:

  1. P+P 中找 Q 的所有出现位置
  2. 每个出现位置导出一个候选 T
  3. 看这个候选 T 是否在集合 T 中出现

为了高效找 Q 的出现位置,使用 KMP;为了高效查候选串是否在 T 集合中,使用哈希。

情况二:N < M

完全对称地处理 T_j

T_j 记成:

  • X:前 half-N 个字符
  • Y:后 half 个字符

此时后一半就是 Y,前一半是 S_i + X
要求前一半是 Y 的某个旋转。

于是转成:

  1. Y+Y 中找 X 的出现位置
  2. 由这些位置反推出候选 S
  3. 在集合 S 中查有没有这个候选

两种情况都只需要做:

  • KMP 找匹配位置
  • 哈希取候选子串
  • 对同一个原串产生的重复候选去重
  • 用哈希表统计另一侧集合中的出现次数

这样就可以在线性级别处理所有字符串内容。

代码

cpp
#include <bits/stdc++.h>
using namespace std;

typedef unsigned long long ull;

const ull BASE1 = 131;
const ull BASE2 = 13331;

struct HashKey {
    ull h1, h2;

    bool operator==(const HashKey &other) const {
        return h1 == other.h1 && h2 == other.h2;
    }

    bool operator<(const HashKey &other) const {
        if (h1 != other.h1) {
            return h1 < other.h1;
        }
        return h2 < other.h2;
    }
};

struct KeyHasher {
    size_t operator()(const HashKey &key) const {
        ull x = key.h1 * 1000003ULL;
        ull y = key.h2 * 1000033ULL;
        return (size_t)(x ^ (y << 1) ^ (y >> 3));
    }
};

int total_s, total_t, n, m;
vector<string> s_list, t_list;
vector<ull> pow1_base, pow2_base;

int char_value(char ch) {
    return (unsigned char)ch + 1;
}

void build_pow(int max_len) {
    pow1_base.assign(max_len + 1, 0);
    pow2_base.assign(max_len + 1, 0);
    pow1_base[0] = 1;
    pow2_base[0] = 1;
    for (int i = 1; i <= max_len; i++) {
        pow1_base[i] = pow1_base[i - 1] * BASE1;
        pow2_base[i] = pow2_base[i - 1] * BASE2;
    }
}

void build_prefix_hash(const string &s, vector<ull> &h1, vector<ull> &h2) {
    int len = (int)s.size();
    h1.assign(len + 1, 0);
    h2.assign(len + 1, 0);
    for (int i = 0; i < len; i++) {
        h1[i + 1] = h1[i] * BASE1 + char_value(s[i]);
        h2[i + 1] = h2[i] * BASE2 + char_value(s[i]);
    }
}

HashKey get_sub_hash(const vector<ull> &h1, const vector<ull> &h2, int l, int len) {
    HashKey res;
    res.h1 = h1[l + len] - h1[l] * pow1_base[len];
    res.h2 = h2[l + len] - h2[l] * pow2_base[len];
    return res;
}

HashKey hash_whole_string(const string &s) {
    ull h1 = 0, h2 = 0;
    for (int i = 0; i < (int)s.size(); i++) {
        h1 = h1 * BASE1 + char_value(s[i]);
        h2 = h2 * BASE2 + char_value(s[i]);
    }
    return {h1, h2};
}

vector<int> prefix_function(const string &pat) {
    int len = (int)pat.size();
    vector<int> pi(len, 0);
    for (int i = 1; i < len; i++) {
        int j = pi[i - 1];
        while (j > 0 && pat[i] != pat[j]) {
            j = pi[j - 1];
        }
        if (pat[i] == pat[j]) {
            j++;
        }
        pi[i] = j;
    }
    return pi;
}

// 在 text 中寻找 pattern 的所有出现位置。
vector<int> kmp_match(const string &text, const string &pattern) {
    vector<int> result;
    if (pattern.empty()) {
        return result;
    }
    vector<int> pi = prefix_function(pattern);
    int j = 0;
    for (int i = 0; i < (int)text.size(); i++) {
        while (j > 0 && text[i] != pattern[j]) {
            j = pi[j - 1];
        }
        if (text[i] == pattern[j]) {
            j++;
        }
        if (j == (int)pattern.size()) {
            result.push_back(i - j + 1);
            j = pi[j - 1];
        }
    }
    return result;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> total_s >> total_t >> n >> m;
    s_list.resize(total_s);
    t_list.resize(total_t);
    for (int i = 0; i < total_s; i++) {
        cin >> s_list[i];
    }
    for (int i = 0; i < total_t; i++) {
        cin >> t_list[i];
    }

    int half = (n + m) / 2;
    int max_len = max(max(n, m), half * 2);
    build_pow(max_len);

    long long answer = 0;

    if (n >= m) {
        int d = n - half;

        unordered_map<HashKey, int, KeyHasher> t_count;
        t_count.reserve(total_t * 2 + 1);
        for (int i = 0; i < total_t; i++) {
            t_count[hash_whole_string(t_list[i])]++;
        }

        for (int i = 0; i < total_s; i++) {
            const string &a = s_list[i];
            string p = a.substr(0, half);
            string q = a.substr(half, d);
            string text = p + p;

            vector<ull> h1, h2;
            build_prefix_hash(text, h1, h2);

            vector<HashKey> cand;
            if (d == 0) {
                cand.reserve(half);
                for (int st = 0; st < half; st++) {
                    cand.push_back(get_sub_hash(h1, h2, st, m));
                }
            }
            else {
                vector<int> pos = kmp_match(text, q);
                cand.reserve(pos.size());
                for (int idx = 0; idx < (int)pos.size(); idx++) {
                    int st = pos[idx];
                    if (st < half) {
                        cand.push_back(get_sub_hash(h1, h2, st + d, m));
                    }
                }
            }

            sort(cand.begin(), cand.end());
            cand.erase(unique(cand.begin(), cand.end()), cand.end());
            for (int j = 0; j < (int)cand.size(); j++) {
                unordered_map<HashKey, int, KeyHasher>::iterator it = t_count.find(cand[j]);
                if (it != t_count.end()) {
                    answer += it->second;
                }
            }
        }
    }
    else {
        int e = half - n;

        unordered_map<HashKey, int, KeyHasher> s_count;
        s_count.reserve(total_s * 2 + 1);
        for (int i = 0; i < total_s; i++) {
            s_count[hash_whole_string(s_list[i])]++;
        }

        for (int i = 0; i < total_t; i++) {
            const string &b = t_list[i];
            string x = b.substr(0, e);
            string y = b.substr(e, half);
            string text = y + y;

            vector<ull> h1, h2;
            build_prefix_hash(text, h1, h2);

            vector<HashKey> cand;
            if (e == 0) {
                cand.reserve(half);
                for (int st = 0; st < half; st++) {
                    cand.push_back(get_sub_hash(h1, h2, st, n));
                }
            }
            else {
                vector<int> pos = kmp_match(text, x);
                cand.reserve(pos.size());
                for (int idx = 0; idx < (int)pos.size(); idx++) {
                    int st = pos[idx];
                    if (st >= n && st < n + half) {
                        cand.push_back(get_sub_hash(h1, h2, st - n, n));
                    }
                }
            }

            sort(cand.begin(), cand.end());
            cand.erase(unique(cand.begin(), cand.end()), cand.end());
            for (int j = 0; j < (int)cand.size(); j++) {
                unordered_map<HashKey, int, KeyHasher>::iterator it = s_count.find(cand[j]);
                if (it != s_count.end()) {
                    answer += it->second;
                }
            }
        }
    }

    cout << answer << '\n';

    return 0;
}

复杂度

设所有字符串总长度为 Lsum

每个字符串只会被做常数次:

  • 构造循环串
  • 跑一次 KMP
  • 对若干匹配位置取哈希并查表

因此总时间复杂度可以看作 O(Lsum+匹配位置总数)O(Lsum + 匹配位置总数),实际实现中再加上一点去重排序的代价。
空间复杂度是 O(Lsum)O(Lsum)

总结

这题最重要的是把“前半和后半互为旋转”拆开,并根据 NM 的大小关系,把问题压缩成:

  • 在一个循环串里找固定前后缀关系
  • 由匹配位置反推出另一侧字符串

本质上是字符串建模 + KMP + 哈希的组合题。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析