题解:洛谷 AT_abc468_d Pre-Palindrome

【题目来源】

洛谷:AT_abc468_d [ABC468D] Pre-Palindrome

【题目描述】

如果一个仅由小写英文字母组成的字符串满足以下条件,则称其为好字符串

  • 只需将至多一个字符替换为任意字符,就可以使其变为回文串。

例如,aiwaiabcdcza 都是好字符串,而 abcdatcoder 不是好字符串。特别地,回文串本身也是好字符串。

给定一个由小写英文字母组成的字符串 \(S\)

请计算 \(S\) 的非空子串(连续子序列)中,有多少个是好字符串。

来自 \(S\) 不同位置的两个子串,即使内容相同,也要分别计数。

什么是子串?
\(S\)子串是通过删除 \(S\) 开头的零个或多个字符和结尾的零个或多个字符得到的字符串。
例如,ababc 的子串,但 ac 不是 abc 的子串。

【输入】

输入从标准输入读取,格式如下:

\(S\)

【输出】

输出答案。

【输入样例】

ababa

【输出样例】

13

【核心思想】

  1. 问题分析:给定字符串 \(S\),求其中有多少个非空子串是"好字符串"——即通过修改最多一个字符可以变成回文串。回文串本身也是好字符串。这是一个中心扩展 + 不匹配计数问题,核心在于利用回文的对称性,从每个可能的中心向两边扩展并统计不匹配字符对数。

  2. 算法选择

    • 中心扩展法:枚举每个可能的回文中心(奇数长度为中心字符,偶数长度为中心缝隙),向两边扩展并统计不匹配数
    • 提前终止:不匹配数超过 \(1\) 时立即停止扩展
  3. 关键步骤

    • 读入数据:读取字符串 \(S\),长度 \(n\)
    • 奇数长度子串(中心 \(mid\)\(0\)\(n-1\)):
      • \(l = r = mid\)\(mis = 0\)
      • \(ans \leftarrow ans + 1\)(长度为 \(1\) 总是好字符串)
      • 向两边扩展(\(l > 0\)\(r < n-1\)):
        • \(l \leftarrow l - 1\)\(r \leftarrow r + 1\)
        • \(mis \leftarrow mis + [s[l] \neq s[r]]\)
        • \(mis \leq 1\)\(ans \leftarrow ans + 1\)
        • 否则 \(break\)
    • 偶数长度子串(中心缝隙 \(mid\)\(0\)\(n-2\)):
      • \(l = mid\)\(r = mid + 1\)\(mis = [s[l] \neq s[r]]\)
      • \(mis \leq 1\)\(ans \leftarrow ans + 1\),然后向两边扩展(同奇数情况)
    • 输出结果\(ans\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n^2)\),最坏情况每个中心扩展 \(O(n)\),共 \(2n\) 个中心
    • 空间复杂度:\(O(1)\),仅使用指针和计数器
  5. 中心扩展与提前终止的核心思想

    • 回文的对称性:从中心向两边扩展时,新加入的字符对 \((s[l], s[r])\) 若相同则不影响回文性,若不同则增加一次"修改需求"
    • 修改次数的单调性:向两边扩展只会增加或保持不匹配数,不会减少。因此一旦 \(mis > 1\),后续扩展必然不满足条件,可直接终止
    • 两种中心类型:奇数长度子串有 \(n\) 个中心点,偶数长度子串有 \(n-1\) 个中心缝隙,覆盖所有可能的子串
    • 计数策略:每个满足 \(mis \leq 1\) 的扩展状态对应一个好字符串子串,直接累加。由于不同位置的相同子串分别计数,无需去重
    • 适用于回文相关子串统计、中心扩展类字符串问题

【算法标签】

普及 #模拟

【代码详解】

#include <bits/stdc++.h>
using namespace std;
#define int long long    // 将int定义为long long,避免子串数量过多导致溢出
string s;               // 输入的字符串S
int ans;                // ans记录好字符串子串的总数

signed main()           // 使用signed main配合#define int long long
{
    cin >> s;           // 读入字符串S
    int n = s.size();   // n为字符串S的长度

    // 第一步:枚举奇数长度子串的中心点(中心为单个字符)
    for (int mid = 0; mid < n; mid++)  // 遍历每个可能的中心位置
    {
        int l = mid, r = mid;          // 初始化左右指针都指向中心点,形成长度为1的子串
        int mis = 0;                   // mis记录当前子串中不匹配的对称位置对数(即需要修改的字符数)
        ans++;                         // 长度为1的子串总是回文串(0次修改),一定是好字符串,直接计数
        while (l > 0 && r < n - 1)     // 向两边扩展,确保不越界
        {
            l--;                       // 左指针向左移动
            r++;                       // 右指针向右移动
            mis += (s[l] != s[r]);     // 如果新扩展的两端字符不同,不匹配数加1
            if (mis <= 1)              // 如果不匹配数不超过1(最多修改1个字符可变成回文)
                ans++;                 // 该子串是好字符串,计数加1
            else                       // 如果不匹配数超过1
                break;                 // 继续扩展只会增加不匹配数,直接退出
        }
    }

    // 第二步:枚举偶数长度子串的中心点(中心为两个相邻字符之间)
    for (int mid = 0; mid < n - 1; mid++)  // 遍历每对相邻字符作为中心
    {
        int l = mid, r = mid + 1;      // 初始化左右指针指向相邻的两个字符,形成长度为2的子串
        int mis = (s[l] != s[r]);      // 初始不匹配数为中间两个字符是否相同(0或1)
        if (mis <= 1)                  // 如果长度为2的子串最多需要修改1个字符
        {
            ans++;                     // 该长度为2的子串是好字符串,计数加1
            while (l > 0 && r < n - 1) // 向两边扩展,确保不越界
            {
                l--;                   // 左指针向左移动
                r++;                   // 右指针向右移动
                mis += (s[l] != s[r]); // 如果新扩展的两端字符不同,不匹配数加1
                if (mis <= 1)          // 如果不匹配数不超过1
                    ans++;             // 该子串是好字符串,计数加1
                else                   // 如果不匹配数超过1
                    break;             // 继续扩展只会增加不匹配数,直接退出
            }
        }
    }

    cout << ans << endl;               // 输出好字符串子串的总数

    return 0;
}

【运行结果】

ababa
13
posted @ 2026-07-28 20:19  团爸讲算法  阅读(11)  评论(0)    收藏  举报