瑞瑞的字符统计

【题目来源】

瑞学堂:瑞瑞的字符统计

【题目描述】

瑞瑞得到了一串由小写字母组成的字符串 \(S\)。他想统计这个字符串中,所有回文子串中,每个字母出现的总次数。

回文串是指正读和反读都一样的字符串。单个字符被视为回文串。

例如,字符串 aba 的回文子串有:a(位置1),b(位置2),a(位置3),aba(整个串)。其中字母 a 出现了 \(4\) 次,字母 b 出现了 \(2\) 次。

由于结果可能很大,请输出每个字母出现次数对 \(10^9+7\) 取模后的结果。

请你帮助瑞瑞编写程序完成这个任务。

【输入】

输入一行一个字符串 \(S\),仅由小写字母组成。

【输出】

输出 \(26\) 个整数,用空格分隔,依次表示字母 a 到 z 在所有回文子串中出现的总次数对 \(10^9+7\) 取模的结果。

【输入样例】

aba

【输出样例】

4 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

【核心思想】

  1. 问题分析:给定字符串 \(S\),求所有回文子串中每个字母出现的总次数(对 \(10^9+7\) 取模)。这是一个Manacher + 差分数组问题,关键在于:先用 Manacher 求出以每个位置为中心的回文半径,再用差分技巧统计每个位置被多少个回文子串覆盖,最后累加各字母的贡献。

  2. 算法选择

    • Manacher 算法:求出以处理后字符串每个位置 \(i\) 为中心的最长回文半径 \(d[i]\)
    • 差分数组(二阶差分):每个中心 \(i\) 的回文串对区间 \([i-d[i]+1, i+d[i]-1]\) 产生"中间高两边低"的三角形贡献,用二阶差分将 \(O(n^2)\) 的区间覆盖优化为 \(O(n)\)
    • 前缀和还原:两次前缀和将差分数组还原为每个位置的实际覆盖次数
  3. 关键步骤

    • Manacher 预处理:插入 # 统一奇偶回文,计算 \(d[i]\)
    • 二阶差分标记(遍历每个中心 \(i\)):
      • 回文覆盖范围 \([l, r] = [i-d[i]+1, i+d[i]-1]\)
      • coeff[l] += 1coeff[i+1] -= 2coeff[r+2] += 1
    • 两次前缀和还原覆盖次数
      • cur += coeff[i](一阶前缀和),sum += cur(二阶前缀和),cnt[i] = sum
    • 统计字母贡献:遍历处理后字符串,对实际字符位置 \(i\)(非 #$),ans[s[i]-'a'] += cnt[i]
    • 去重修正:每个回文子串被计算了两次(奇数中心和偶数中心),最终答案乘 \(2\) 的逆元 \((MOD+1)/2\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n)\),Manacher \(O(n)\),差分标记和前缀和 \(O(n)\)
    • 空间复杂度:\(O(n)\),处理后字符串、\(d\) 数组、差分数组等
  5. Manacher + 差分的核心思想

    • 回文覆盖的三角形分布:以 \(i\) 为中心、半径为 \(R\) 的回文串,位置 \(j\) 被覆盖当且仅当 \(|j-i| < R\),覆盖次数随距离中心增加而递减,形成三角形贡献
    • 二阶差分转常数操作:三角形数列的二阶差分为常数,通过 +1, -2, +1 的标记将 \(O(n^2)\) 的逐点覆盖降为 \(O(1)\) 的区间标记
    • 对称性去重:插入 # 后,每个实际回文子串既对应某个原字符中心(奇数长度)也对应某个 # 中心(偶数长度),总贡献被计算两次,需除以 \(2\)
    • 模运算技巧:用乘法逆元代替除法,避免浮点精度问题
    • 适用于"统计所有回文子串中各位置/字符贡献"的问题,核心在于将回文结构转化为区间覆盖,再用差分优化统计

【算法标签】

Manacher

【代码详解】

#include <bits/stdc++.h>
using namespace std;
#define int long long    // 将int定义为long long,避免中间计算溢出
const int N = 100005 * 3, MOD = 1e9 + 7;  // N为处理后字符串最大长度,MOD为模数
char a[N], s[N];         // a存储原始字符串,s存储处理后的字符串(插入分隔符#)
int d[N];                // d[i]为Manacher算法中以i为中心的最长回文半径
int coeff[N];            // coeff用于差分数组,记录每个位置作为回文中心次数的贡献
int cnt[N], ans[26];     // cnt[i]为位置i在所有回文子串中被覆盖的总次数,ans[26]记录26个字母的出现次数

// Manacher算法核心函数:计算以每个位置为中心的最长回文半径
void get_d(char *s, int n)
{
    d[1] = 1;            // 初始化:以第一个字符为中心的回文半径为1
    // i遍历每个中心位置,l和r维护当前最右回文串的左右边界
    for (int i = 2, l, r = 1; i <= n; i++)
    {
        // 如果当前位置i在当前最右回文串[r]的范围内,利用对称性初始化d[i]
        if (i <= r)
            d[i] = min(d[r - i + l], r - i + 1);
        // 中心扩展:尝试向两边扩展回文串
        while (s[i - d[i]] == s[i + d[i]])
            d[i]++;
        // 更新最右回文串边界
        if (i + d[i] - 1 > r)
            l = i - d[i] + 1, r = i + d[i] - 1;
    }
}

signed main()            // 使用signed main配合#define int long long
{
    scanf("%s", a + 1);  // 读入原始字符串
    int n = strlen(a + 1), k = 0;  // n为原始字符串长度
    // 预处理:在原始字符串的每两个字符之间以及首尾插入分隔符'#'
    s[0] = '$';          // s[0]放哨兵字符$,防止越界
    s[++k] = '#';        // 第一个字符为#
    for (int i = 1; i <= n; i++)
    {
        s[++k] = a[i];   // 放入原始字符
        s[++k] = '#';    // 在每个字符后插入#
    }
    n = k;               // 更新n为处理后字符串的长度
    get_d(s, n);         // 执行Manacher算法

    // 第一步:利用差分数组统计每个位置被多少个回文子串覆盖
    for (int i = 1; i <= n; i++)
    {
        int R = d[i];    // R为以i为中心的回文半径
        if (R <= 1)
            continue;    // 半径为1表示只有自身(单个#),无实际字符贡献

        // 回文串在处理后字符串中的覆盖范围
        int l = i - R + 1;  // 左边界
        int r = i + R - 1;  // 右边界

        // 差分标记:以i为中心的回文串对区间[l,r]内每个位置的贡献
        // 使用二阶差分技巧,将三角形贡献转化为常数差分
        coeff[l] = (coeff[l] + 1) % MOD;      // 左端点:一阶差分+1
        // 顶点右侧:一阶差分从+1变-1,净变化-2
        coeff[i + 1] = (coeff[i + 1] - 2 + MOD) % MOD;
        // 右端点外:一阶差分从-1变0
        coeff[r + 2] = (coeff[r + 2] + 1) % MOD;
    }

    // 第二步:通过两次前缀和还原每个位置被覆盖的次数
    int cur = 0, sum = 0;
    for (int i = 1; i <= n; i++)
    {
        cur = (cur + coeff[i]) % MOD;    // 一阶前缀和(当前一阶差分值)
        sum = (sum + cur) % MOD;         // 二阶前缀和(当前位置被覆盖的总次数)
        cnt[i] = sum;                    // 记录位置i被覆盖的次数
    }

    // 第三步:统计每个实际字母的出现次数
    for (int i = 1; i <= n; i++)
    {
        // 只统计实际字符位置(非#且非$的位置,即原始字符串的字符位置)
        if (s[i] != '#' && s[i] != '$')
        {
            ans[s[i] - 'a'] = (ans[s[i] - 'a'] + cnt[i]) % MOD;  // 累加该位置被覆盖的次数
        }
    }

    // 第四步:输出结果
    // 每个实际回文子串在Manacher中被计算了两次(奇数中心和偶数中心各一次),所以答案要除以2
    int INV2 = (MOD + 1LL) / 2;  // 2在模MOD下的逆元(MOD为质数,且MOD为奇数,(MOD+1)/2即为2的逆元)
    for (int i = 0; i < 26; i++)
    {
        ans[i] = ans[i] * INV2 % MOD;  // 除以2(乘逆元)
        cout << ans[i] << " ";          // 输出26个字母的结果
    }
    cout << endl;
    return 0;
}

【运行结果】

aba
4 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
posted @ 2026-08-07 11:44  团爸讲算法  阅读(4)  评论(0)    收藏  举报