算法提升之字符串回文处理-(manacher)

今天给大家分享的就是关于manacher算法的解题方法与思路。

先明确几个关键变量的含义

  • t数组:经过预处理的字符串(插入#和边界符),长度为2n+3n是原字符串s的长度),用于统一处理奇、偶长度的回文。
  • res:从t数组两端开始,连续对称的最大长度(即最长的等长前缀和后缀对称部分,对应原字符串中res//2个字符,因为t数组有#分隔)。
  • p[i]:Manacher 算法计算的回文半径数组,p[i]表示以t[i]为中心的最长回文串向左右扩展的长度(原字符串中回文长度为p[i]-1)。
  • i:当前遍历的t数组的索引(范围1~2n+1)。

第一题:
问题描述

给定一个字符串 S ,请找出 S 的一个前缀和后缀,使得它们拼接后是一个回文串。

请输出这个串的最长长度。

输入描述

输入一行包含一个字符串 S ,由小写英文字母组成。

输出描述

输出一行包含一个整数表示答案。

输入案例:

aababa

输出案例:

7

代码部分:

//所求的串由两部分组成:
//1.等长的前后缀拼成的回文串
//2.原串内部的回文串
#include<bits/stdc++.h>
using namespace std;
const int N=4e5+40;
char s[N],t[N];
int p[N];
int main()
{
    cin>>s+1;
    int n=strlen(s+1);
    t[0]='^',t[2*n+2]='$';
    for(int i=1;i<=2*n+1;++i)t[i]=i&1?'#':s[i/2];
  //求等长前后缀拼成的回文串的最大长度
    int res=0;
    for(int i=1;i<=2*n+1&&t[i]==t[2*n+2-i];++i)res++;
  //manache算法求内部回文串最大长度
    int c=0,r=0;
    for(int i=1;i<=2*n+1;++i){
        p[i]=i<r?min(r-i,p[2*c-i]):1;
        while(t[i+p[i]]==t[i-p[i]])p[i]++;
        if(i+p[i]>r)c=i,r=i+p[i];
    }
  //枚举内部回文串,更新答案
    int ans=0;
    for(int i=1;i<=2*n+1;++i)
    {
        if(res+p[i]>=i)ans=max(ans,i-1);
        if(res+p[i]>=2*n+2-i)ans=max(ans,2*n+1-i);
    }
    cout<<ans<<'\n';
    return 0;
}

注意点⚠️:

1.res的作用:

s = "abddca"n=6),预处理后的t数组(插入#和边界符)为:

t = ^ # a # b # d # d # c # a # $
索引:0 1 2 3 4 5 6 7 8 9 10 11 12 13

其中,t的有效长度为2*n+1=13(索引 1~13),对称位置为i2*n+2 - i = 14 - i

循环过程:

  • i=1t[1] = '#'t[13] = '#' → 相等,res=1
  • i=2t[2] = 'a't[12] = 'a' → 相等,res=2
  • i=3t[3] = '#'t[11] = '#' → 相等,res=3
  • i=4t[4] = 'b't[10] = 'c' → 不相等,循环停止。

最终res=3

2.

1. 第一种情况:if(res + p[i] >= i) ans = max(ans, i-1)
  • 含义:判断 “左侧等长对称前缀” 与 “以i为中心的内部回文串” 能否拼接成回文串,并计算其长度。

  • 拆解

    • res:左侧等长对称前缀的长度(从t[1]t[res])。
    • p[i]:以i为中心的内部回文串向左扩展的最大长度(覆盖范围[i-p[i]+1, i+p[i]-1])。
    • res + p[i] >= i:表示左侧等长对称前缀(res)与内部回文串的左扩展部分(p[i])能够 “衔接” 起来,覆盖到i的位置(即两者可以拼接成连续的回文)。
    • i-1:此时拼接后的回文串在t数组中的长度(转换为原字符串长度时需除 2,但这里ans直接存储t数组中的有效长度,最终输出时已对应原字符串长度)。
  • 举例
    res=3(左侧前 3 个字符对称),i=5p[i]=3(内部回文向左扩展 3 个单位),则res + p[i] = 6 >= 5,说明左侧对称前缀与内部回文可以衔接,拼接后的回文长度为i-1=4(对应原字符串长度 2)。

2. 第二种情况:if(res + p[i] >= 2*n+2 - i) ans = max(ans, 2*n+1 -i)
  • 含义:判断 “右侧等长对称后缀” 与 “以i为中心的内部回文串” 能否拼接成回文串,并计算其长度。

  • 拆解

    • 2*n+2 - it数组中i对应的右侧对称位置(例如i=2n=3时,对称位置为2*3+2 -2=6)。
    • res + p[i] >= 2*n+2 -i:表示右侧等长对称后缀(res)与内部回文串的右扩展部分(p[i])能够 “衔接” 起来,覆盖到右侧对称位置(即两者可以拼接成连续的回文)。
    • 2*n+1 -i:此时拼接后的回文串在t数组中的长度(同样对应原字符串长度)。
  • 举例
    res=3(右侧后 3 个字符对称),i=5p[i]=3(内部回文向右扩展 3 个单位),右侧对称位置为2*3+2 -5=3,则res + p[i] = 6 >= 3,说明右侧对称后缀与内部回文可以衔接,拼接后的回文长度为2*3+1 -5=2(对应原字符串长度 1)。

为什么这样计算能得到最长回文串?

  1. 回文的对称性:回文串的核心是左右对称,res提供了 “天然对称” 的前缀和后缀基础,而p[i]提供了内部的对称部分。
  2. 拼接逻辑:当 “外部对称部分”(res)与 “内部对称部分”(p[i])能够衔接时,两者拼接后形成的整体必然也是对称的(即回文串)。
  3. 遍历所有可能:通过遍历t数组的每个位置i,覆盖了所有可能的内部回文中心,确保不会遗漏最长的拼接回文串。

3.res+p[i]>=i的理解:

res + p[i] >= i 的本质:覆盖左侧边界

这个条件的核心是判断:左侧的 “等长对称前缀”(长度 res)与 “以 i 为中心的内部回文串”(半径 p[i])能否拼接起来,覆盖到 i 这个位置

拆解如下:

  1. res 的意义
    res 是从 t 数组最左侧(t[1])开始,连续与右侧对称位置匹配的长度。例如 res=3 表示 t[1]t[2]t[3] 分别与右侧对称位置的字符相等。
    它代表了字符串最左侧的 “天然对称部分”,可以直接作为回文串的左半部分。

  2. p[i] 的意义
    p[i] 是以 i 为中心的最长回文串的半径(向左右扩展的长度)。例如 p[i]=4 表示以 i 为中心,向左扩展 3 个位置、向右扩展 3 个位置的子串是回文(总长度 2*4-1=7)。
    它代表了字符串内部的一个回文子串。

  3. res + p[i] >= i 的逻辑

    • i 是内部回文串的中心位置,i 左侧的区域需要被覆盖才能形成完整回文。
    • res 覆盖了从左侧起点(t[1])到 t[res] 的范围。
    • p[i] 覆盖了从 i-p[i]+1 到 i 的范围(内部回文串的左半部分)。
    • 当 res + p[i] >= i 时,说明左侧的 res 区域与内部回文的左半部分(p[i])能够 “无缝衔接”,覆盖到中心 i,此时两者拼接后可以形成一个更长的回文串。

第二题:

题目描述

小蓝有一个只包含 0 和 1 字符串,请你帮他算算这个字符串有多少个子串将 00 和 11 取反后,再将整个子串反过来和原子串一样。

输入描述

输入第一行包含一个整数 n(n≤5×105),表示字符串长度。

第二行包含一个长度为 n 的字符串 S,S 只拥有 0、1 两种字符。

输出描述

输出仅一行,包含一个整数,表示答案。

输入案例:

6
100100

输出案例:

3

代码部分:

#include <bits/stdc++.h>
using namespace std;
typedef  long long ll;
const int P=131;
const int N=5e5+10;
long long ans;
char s1[N],s2[N];
int n;
ll h1[N],h2[N],p[N];
ll get(ll h[],int l,int r)
    {return h[r]-h[l-1]*p[r-l+1];}
int main()
{
  cin>>n;
  for(int i=1;i<=n;i++)
  {
    char t;
    cin>>t;
    s1[i]=t;
    s2[i]=(t=='1'?'0':'1');
  }
  reverse(s2+1,s2+n+1);
  p[0]=1;
  for(int i=1;i<=n;i++)
  {
      p[i]=p[i-1]*P;
      h1[i]=h1[i-1]*P+s1[i];
      h2[i]=h2[i-1]*P+s2[i];
  }
  for(int i=1;i<n;i++)
  {
      for(int j=i+1;j<=n;j++)
      {
          if(get(h1,i,j)==get(h2,n-j+1,n-i+1))
           ans++;
    }
  }
  cout<<ans;
  return 0;
}

这是用的hash的算法思想,注意的是用reverse来求h2,最后get(h2,n-j+1,n-i+1)这里也要取反⚠️

接下来还有一种用manacher的思路解决的方法:

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define double long double
#define ls o << 1
#define rs o << 1 | 1
// #define mid ((st + ed) >> 1)
typedef double db;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 0x3f3f3f3f3f3f3f3f;
const int base = 131;
const int mod = 998244353;
const int maxn = 5e5 + 5;
int lowbit(int x) { return x & (-x); };
int s[maxn], n, p[maxn];
void solve()
{
  //freopen("date.in", "r", stdin);
  //freopen("date1.out", "w", stdout);
  cin >> n;
  string tmp;
  cin >> tmp;
  for (int i = 1; i <= n; i++)
    s[i] = ((tmp[i - 1] - '0') ? 1 : -1);
  s[0] = 2, s[2 * n + 2] = 3;
  for (int i = 2 * n + 1; i >= 1; i--)
  {
    if (i & 1)
      s[i] = 0;
    else
      s[i] = s[i >> 1];
  }
  int c = 0, r = 0;
  for (int i = 1; i <= 2 * n + 1; i += 2)
  {
    p[i] = i < r ? min(r - i, p[2 * c - i]) : 1;
    while (s[i + p[i]] == -s[i - p[i]])
      p[i]++;
    if (i + p[i] > r)
      c = i, r = i + p[i];
  }
  int ans = 0;
  for (int i = 1; i <= 2 * n + 1; i += 2)
  {
    int len = p[i] - 1;
    ans += len / 2;
  }
  cout << ans;
}
signed main()
{
  ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  int _ = 1;
  // cin >> _;
  while (_--)
    solve();
  return 0;
}

这题是马拉车算法的变体,我们知道马拉车算法的原理就是根据了回文串中最小回文半径优化的时间复杂度,例如abacabac,这段字符串,以c为回文中心,左边的b回文半径为2,则右边的b回文半径至少为2(根据对称性),然后通过暴力向右查找有没有更大的回文半径。那么这题能否借鉴这个思路?

显然是可以的,比如010101,我们定义合法串为关于中心对称的两数字必须为一个0和一个1(注:合法串并非全答案,答案的合法串中心为空,后面会解决这个问题)。则再看这个0101串,提取出合法串0101001010,以第二个0为回文中心左边为01,右边与之对应为1010,可以看到这两个都是合法串,右边合法串的长度是不是至少为2?显然是的,因为右边的串是由左边的串反转得来的,左边的串合法,右边一定合法。

那么像马拉车算法一样,为了更好的表示回文半径,我们把合法串的定义改一改,关于中心对称的左右两个数相加等于0,我们把1不动,0改为−1,−1和1中间补0,是不是就是完完全全变成马拉车算法的板子了?并且,由于我们需要合法串的长度一定为偶数,意思就是我们不能以1或是−1为中心,所以遇到他们直接跳过,只判断0为中心即可,并且以0合法半径一定为奇数。得到合法半径r,原合法01串长度为r−1,其中包含了2r−1​个合法字串,把他们累加起来即可。

时间复杂度O(n)

好了今天的分享就到这里,希望对大家能有所帮助,希望大家多多关注。

### Manacher Algorithm for Palindrome Strings Manacher's algorithm 是一种高效的线性时间复杂度 \(O(n)\)算法,用于找到给定字符串中的最长回文子串。它通过利用回文的对称性质来减少不必要的计算。 #### 算法核心思想 为了处理偶数长度和奇数长度的回文字串统一化问题,可以在原字符串中插入特殊分隔符 `#` 和边界标记 `$` 来构建一个新的字符串。例如,对于输入字符串 `"aab"`,可以将其转换为 `"$#a#a#b#@"`[^1]。这样做的好处是可以将所有可能的回文中心标准化到单个字符上。 接着定义一个辅助数组 `P[]`,其中 `P[i]` 表示以第 i 个位置为中心的最大半径(即该回文右端点减去左端点再加一除二的结果)。同时维护两个变量:当前已知最右侧回文的中心 `center` 和其对应的右边界 `right`。 当遍历新字符串时,如果当前位置小于等于右边界的覆盖范围,则尝试基于镜像位置的信息加速判断;否则直接从头开始扩展直到不满足条件为止,并更新全局最优解以及必要情况下调整 center 和 right 值。 以下是 Python 实现代码: ```python def manachers_algorithm(s): # Preprocess the string by inserting special characters '#' T = ['#'] * (2 * len(s) + 3) T[0], T[-1] = '$', '@' # Sentinels j = 0 for i in range(1, len(T)-1, 2): T[i] = s[j] j += 1 P = [0] * len(T) C = R = 0 max_len = 0 center_index = 0 for i in range(1, len(T)-1): mirror_i = 2*C - i if R > i: P[i] = min(R-i, P[mirror_i]) while T[i+(1+P[i])] == T[i-(1+P[i])]: P[i] += 1 if i + P[i] > R: C = i R = i + P[i] if P[i] > max_len: max_len = P[i] center_index = i start = (center_index - max_len) // 2 return s[start:start+max_len] print(manachers_algorithm("banana")) # Output: 'anana' ``` 此函数接受原始字符串作为参数并返回最长回文子串。 --- ### KMP Algorithm Usage in Palindromes 尽管 KMP 主要应用于模式匹配领域而非专门针对回文检测设计,但它也可以间接帮助解决某些涉及前缀与后缀关系的问题。比如,在寻找特定类型的回文中可能会用到它的部分特性——具体来说就是如何高效比较前后缀相似性的技巧。 然而需要注意的是,标准形式下的 KMP 并不适合单独用来判定整个字符串是否构成完全意义上的回文结构或者定位内部存在的最大规模回文片段等问题场景下表现不如其他专用方法那样理想。因此更多时候我们会看到人们倾向于采用诸如动态规划、中心扩散法或者是上述提到过的 Manacher’s Algorithm 这些更适合此类任务的技术手段来进行操作。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值