字符串

📚 字符串算法实战与进阶笔记

第一阶:基础回文子串 (Manacher 算法模板)

关联文件P_3805_模板_Manacher.cpp / mancher.cpp

  • 抽象模型:给定一个字符串,求其内部最长的回文子串长度。
  • 核心痛点:回文串有奇数长度(中心是字符)和偶数长度(中心是间隙)两种情况,分类讨论非常繁琐。暴力向两边扩展的复杂度高达 $O(n^2)$。
  • 破局思路(Manacher 算法)
  • 预处理插桩:在字符之间以及字符串首尾插入无关字符(如 #),将所有回文串强行转化为奇数长度。
  • 维护“最远保护伞”:维护当前所有已知的回文串中,右边界最远的那个回文串(中心为 j,右边界为 j + r[j])。
  • 利用对称性加速:当遍历到新的中心 i 时,如果 i 在“保护伞”内部,可以直接找到 i 关于 j 的对称点 2*j - i。此时 r[i] 至少等于 min(r[2*j - i], 保护伞右边界 - i)。这样跳过了大量不必要的字符比较,将总时间复杂度严格降至 $O(n)$。
  • 黄金结论:在插桩后的字符串中,某位置的回文半径减 1(R - 1),正好等于原字符串中该回文串的实际长度。
点击查看代码
vector<int> manacher(const string &s) {
    // 上面的预处理去除奇偶分讨的情况
    string t;
    t += '#';
    for (auto tmp : s) {
        t += tmp;
        t += '#';
    }
    int n = t.size();

    vector<int> r(n);//维护最远的伞
    for (int i = 0, j = 0; i < n; i++) { // j 是最远的伞
        // 如果最远的伞包含住了 i 可以更新 注意不能超界
        if (j + r[j] > i && 2 * j - i >= 0) {
            r[i] = min(r[2 * j - i], j + r[j] - i);
        }
        // 暴力
        while (i - r[i] >= 0 && i + r[i] < n && t[i - r[i]] == t[i + r[i]])
            r[i]++;

        // 更新最远的伞
        if (i + r[i] > j + r[j]) j = i;
    }
    return r;
}

void solve() {
    string s;
    cin >> s;
    vi r = manacher(s);
    int max_len = 0;
    int best_center = 0;

    for (int i = 0; i < r.size(); i++) {
        int len = r[i] - 1;
        if (len > max_len) {
            max_len = len;
            best_center = i;
        }
    }
    cout << max_len;
}


第二阶:区间子串快速比对 (String Hash 模板)

关联文件字符串哈希.txt

  • 抽象模型:在 O(1) 的时间内,快速判断一个序列(或字符串)的任意两个子区间是否完全相同。
  • 核心思路(多项式滚动哈希)
  • 映射转码:将字符串看作是一个 Base 进制的数字,将其转化为一个巨大的整数,并对某个大质数 $Mod$ 取模。

前缀和思想:预处理出前缀哈希数组 h 和 $Base$ 的次幂数组 p 。查询区间 [l, r] 的哈希值时,利用公式 (h[r] - h[l - 1] * p[r - l + 1] % mod + mod) % mod 即可 。

双哈希防碰撞:为了极大地降低哈希冲突(Hash Collision)的概率,模板采用了双哈希机制。同时使用两套不同的质数模数(1000000007 和 1000000009)进行运算 。

防坑细节:如果对整型数组(如 vector<int>)进行哈希,且数据中存在 0,强烈建议对所有元素加上一个偏移量(offset,例如 +1),防止前导 0 导致不同序列哈希值相同 。

点击查看代码
template<int N>
struct StringHash {
    static constexpr int base[2] = {13331, 131};
    static constexpr int mod[2] = {1000000007, 1000000009};
    std::vector<std::array<int, N> > p, h;

    StringHash() = default;

    StringHash(const std::string &s) { //如果要用vector<int> 得 改成vector<int>  记得如果 int数据中有0 得 + 1(offset) 
        int n = s.size() - 1;
        p.resize(n + 1);
        h.resize(n + 1);
        std::fill(p[0].begin(), p[0].end(), 1);
        for (int i = 1; i <= n; i++)
            for (int j = 0; j < N; j++)
                p[i][j] = 1ll * p[i - 1][j] * base[j] % mod[j];
        for (int i = 1; i <= n; i++)
            for (int j = 0; j < N; j++)
                h[i][j] = (1ll * h[i - 1][j] * base[j] + s[i]) % mod[j];
    }

    std::array<int, N> query(int l, int r) {
        assert(r >= l - 1);
        std::array<int, N> ans;
        if (l > r) return {0, 0};
        for (int i = 0; i < N; i++) {
            ans[i] = (h[r][i] - 1ll * h[l - 1][i] * p[r - l + 1][i] % mod[i] + mod[i]) % mod[i];
        }
        return ans;
    }
};

第三阶:状态递推与回文结合 (最长双回文串)

关联文件P_4555_国家集训队_最长双回文串.cpp

  • 抽象模型:求一个字符串中最长的“双回文串”长度。双回文串定义为两个非空的回文子串拼接而成(即 S = A + B,且 A, B 均为回文)。
  • 核心思路
  • Manacher 提取特征:先跑一遍 Manacher 算法,求出每个位置的最大回文半径 r[i]
  • 维护边界极值:核心在于求出“以某个位置为结尾的最长回文长度 L“以某个位置为开头的最长回文长度 R”。在遍历 Manacher 的半径时,如果以 i 为中心,它的左边界就是 i - len,右边界就是 i + len。利用这个信息更新 L[right]R[left]
  • 状态内推(巧妙的递推):Manacher 求出的是极限半径,但长回文内部一定包含短回文。所以需要进行一次向内的递推覆盖:L[i-2] = max(L[i-2], L[i]-2)R[i+2] = max(R[i+2], R[i]-2)
  • 分割点枚举:最后遍历所有合法的分割点(由于插了 #,偶数索引正好代表字符之间的间隙),寻找 max(L[i] + R[i])。
点击查看代码
vector<int> manacher(const string &s) {
    // 上面的预处理去除奇偶分讨的情况
    string t;
    t += '#';
    for (auto tmp : s) {
        t += tmp;
        t += '#';
    }
    int n = t.size();

    vector<int> r(n);
    for (int i = 0, j = 0; i < n; i++) { // j 是最远的伞
        // 如果最远的伞包含住了 i 可以更新 注意不能超界
        if (j + r[j] > i && 2 * j - i >= 0) {
            r[i] = min(r[2 * j - i], j + r[j] - i);
        }
        // 暴力
        while (i - r[i] >= 0 && i + r[i] < n && t[i - r[i]] == t[i + r[i]])
            r[i]++;

        // 更新最远的伞
        if (i + r[i] > j + r[j]) j = i;
    }
    return r;
}
void solve() {
    string s;
    cin >> s;
    vi r = manacher(s);
    int n = r.size();
    vi L(n), R(n);
    for (int i = 0; i < r.size(); i++) {
        int len = r[i] - 1;
        int left = i - len;
        int right = i + len;

        L[right] = max(L[right], len);
        R[left] = max(R[left], len);
    }
    for (int i = n - 1; i >= 2; i -= 2) {
        L[i - 2] = max(L[i - 2], L[i] - 2);
    }
    for (int i = 0; i < n - 2; i += 2) {
        R[i + 2] = max(R[i + 2], R[i] - 2);
    }
    int ans = 0;
    for (int i = 0; i < n; i += 2) {
        if (L[i] > 0 && R[i] > 0) ans = max(ans, L[i] + R[i]);
    }
    cout << ans << endl;
}

🔴 第四阶:哈希二分与组合逻辑综合 (2227D 变形应用)

关联文件2227D.cpp

  • 抽象模型:判断数组的某个复杂区间组合能否构成回文,寻找最优的分割点,并最终求出该区间的 MEX(未出现的最小非负整数)值。
  • 破局思路
  • 抛弃 Manacher,拥抱哈希二分:由于题目涉及对原数组 a 和反转数组 b 进行跨区间的动态回文判定,Manacher 的连续性遭到破坏。此时,“正向 Hash == 反向 Hash” 就成了判断回文的最强武器。
  • $O(log N)$ 确定回文半径:通过二分查找回文半径 mid,配合 $O(1)$ 的哈希查询(is_palindrome),可以快速找到以特定分割点为中心的最大回文覆盖范围(find_max_2 逻辑)。
  • MEX 统计:拿到合法的回文区间 [L, R] 后,开一个布尔数组 vis 记录区间内出现的数字,从 $0$ 开始向上遍历找第一个未被标记的数字,即为 MEX 值。这是典型的将“字符串高级判定(哈希二分)”与“数组基础统计”结合的复合型题目。
点击查看代码
void solve() {
    int n;
    cin >> n;
    vi a(2 * n + 1), b(2 * n + 1);
    for (int i = 1; i <= 2 * n; i++) {
        cin >> a[i];
    }
    for (int i = 1; i <= 2 * n; i++) {
        b[i] = a[2 * n - i + 1];
    }
    StringHash<2> Z(a);
    StringHash<2> F(b);
    auto is_palindrome = [&](int L, int R) { return Z.query(L, R) == F.query(2 * n - R + 1, 2 * n - L + 1); };
    int P1 = 0, P2 = 0;
    for (int i = 1; i <= 2 * n; i++) {
        if (a[i] == 0) {
            if (P1 == 0)
                P1 = i;
            else
                P2 = i;
        }
    }

    auto find_max_2 = [&](int sum) -> pii {
        int Center_L = sum / 2;
        int Center_R = sum - Center_L;
        int l = -1;
        int r = min(Center_L - 1, 2 * n - Center_R) + 1; // 注意 R 不能越界 否则会RE
        while (l + 1 != r) {
            int mid = (l + r) >> 1;
            if (is_palindrome(Center_L - mid, Center_R + mid))
                l = mid;
            else
                r = mid;
        }

        return make_pair(Center_L - l, Center_R + l);
    };
 
    auto get_mex = [&](int L, int R) {
        vector<bool> vis(n + 1, false);

        for (int i = L; i <= R; i++) {
            int real_val = a[i];
            vis[real_val] = true;
        }

        for (int i = 0; i <= n; i++) {
            if (!vis[i]) {
                return i;
            }
        }
        return n;
    };

    int ans = 0;
    auto get = [&](int P1) {
        auto [L, R] = find_max_2(P1);
        if (L <= R) {
            ans = max(get_mex(L, R), ans);
        }
    };

    get(P1 + P1);
    get(P2 + P2);
    get(P1 + P2);

    cout << ans << "\n";
}

posted @ 2026-05-15 22:04  r_123  阅读(14)  评论(0)    收藏  举报