题意:
给出一个字符串,求其子串中不包含长度大于等于k的回文串的子串个数
思路:
对于单串回文问题,考虑使用manacher算法进行预处理,即可处理出以每个位置为中点的最长回文半径
使用双指针技术,结合回文半径的信息,处理出以每个位置为起点,在不构成大于等于k的回文串前提下,向后能到达的最远位置
对于任何一个大于k的回文串,我们都可以将其看做长度为k或k+1的回文串,因为只要包含长度大于等于k的回文串就不合法,而任何一个长度大于k的回文串都一定包含一个长度为k或k+1的回文子串,所以只需要考虑最短的情况
由于manacher算法会对原串进行扩展,导致每个字符的下标发生变化,处理时只需要/2即可还原成原位置的下标
对于一个长度为n的字符串,其子串个数为n*(n+1)/2,对于我们处理出的每个区间,累加求和
因为两个相邻区间可能相互包含,对于被包含的部分还要容斥处理,减去多加的部分
代码:
#include<iostream>
#include<cstring>
#include <string>
#include<algorithm>
using namespace std;
const int maxl=2e5+5;
const int inf=0x3f3f3f3f;
int p[2*maxl+5]; //p[i]-1表示以i为中点的回文串长度
int map[maxl];
int k;
void Manacher(string s)
{
string now;
int len=s.size();
for(int i=0;i<len;i++) //将原串处理成%a%b%c%形式,保证长度为奇数
{
now+='%';
now+=s[i];
}
now+='%';
len=now.size();
int pos=0,R=0;
for (int i=0;i<len;i++)
{
if (i<R) p[i]=min(p[2*pos-i],R-i); else p[i]=1;
while (0<=i-p[i]&&i+p[i]<len&&now[i-p[i]]==now[i+p[i]]) p[i]++;
if (i+p[i]>R) {pos=i;R=i+p[i];}
}
for (int i=0;i<len;i++) //预处理区间信息
{
if(p[i]-1<k) continue; //只考虑长度大于等于k的回文串
int flag=(p[i]-1-k)%2; //判断该串是包含长度为k的回文子串还是长度为k+1的回文子串
int ln;
if(flag)
ln=k+1;
else
ln=k;
int lx=(i-ln+1)/2; //左指针指向起点,下标还原成原串
int rx=(i+ln-1)/2; //右指针指最长可达位置,下标还原成原串
map[lx]=min(map[lx],rx); //一个起点可能被更新多次,只取最小的结果
}
}
string a;
int main()
{
std::ios::sync_with_stdio(false);
int t;
cin>>t;
while(t--)
{
cin>>k;
cin>>a;
memset(map,inf,sizeof(map));
Manacher(a);
int len=a.size();
int now=len;
for(int i=len;i>=0;i--) //求出以每个点为起点,在不包含大于等于k长度回文子串前提,所能到达的最远位置
{
if(map[i]!=inf)
now=map[i];
map[i]=now;
}
long long ans=0;
long long pre=0;
for(int i=0;i<len;i++)
{
long long tmp=map[i]-i;
ans+=(1+tmp)*tmp/2; //对于长度为tmp的字符串,计算其子串个数
if(pre>=i) //相邻两区间存在交叉,容斥处理
{
tmp=pre-i;
ans-=(1+tmp)*tmp/2; //减去多加的部分
}
pre=map[i];
}
cout<<ans<<endl;
}
return 0;
}