本博客用于对回文串有一定基础的人,而不是教学。(因为写的不是很具体,更多的是给个大局思维,新人可能看不懂qwq)
应该是我写过目前最长的博客了(码字量应该是)
目录
首先是给出一个例子:
求一串字符串中的回文串的个数。
基础思路:暴力
拿到这道题我们知道暴力求解回文串复杂度是O(n)
i表示开头字符,j表示结尾字符,然后判断是不是回文串
每个操作一遍O(n),就是n^3,这也太大了。
然后我们发现回文串是对称的,那么如果从中间开始找是不是回文串就可以减少时间复杂度。于是乎,双指针思路:
对于i,分两种,一种让(l=i,r=i)一种让(l=i-1,r=i),每次判断让l--,r++,然后遇到字符串继续执行并++cnt,否则跳出来。
好在哪里?
此时由于字符串的对称,查找是否为字符串和让查找范围变大是同时进行的,这从原来的j,k两层变成了,不断两边延伸的思想。
这是我能想到的利用回文串的对称性一种改进方式。
后面就不是我的小笨脑子能想到的了。。
先放这里,好几种回文串思路。
但首先要说明,这些东西要活用,比如求最长回文串也能用于求回文串长度,从中间找也能求出末尾为X的回文串(特判一下就好了)。
不活用,就白学
回文串进阶思路:马拉车,dp(跳跃找和非跳跃找),回文树。
后面的内容慢慢补()
进阶思路1:哈希二分
哈希二分运用超级广泛,替换KMP,处理回文串……反正有字符串就有他的活。
思路:
哈希简单讲就是把字符串变成数字,a~z看成一种26进制的数字。
其中p数组是一种处理26进制的权,26^0,26^1,26^2
但我们一般不用26进制,取一个质数(例如1331),因为字符串大了会超出界限,质数不容易重复(这也是为什么用ull的原因,ull可以让哈希值没有负的,好处理)
哈希板子
//得到一截字符串的哈希值
const int P=13331;
ull p[N],h[N];
char s[N];
ull get(int l, int r){
return h[r]-h[l-1]*p[r-l+1];
}
//进制转换字符串获得哈希值
p[0]=1;h[0]=0;
rep(i,1,n){
h[i]=h[i-1]*P+(s[i]-'a'+1);
p[i]=p[i-1]*P;
}
将字符串变成数字之后,二分查找就能减少复杂度。
二分的是回文串的半径
回文串思路肯定是判断i左边和右边同长度是不是一样
因为两边的字符串要反一下,所以还要在哈希板子前提下多加一个get是倒着维护的。
板子(题目不同,看思路)
#include <bits/stdc++.h>
#define int long long
#define ull unsigned long long
#define CIO std::ios::sync_with_stdio(false)
#define rep(i, l, r) for (int i = l; i <= r; i++)
#define nep(i, r, l) for (int i = r; i >= l; i--)
using namespace std;
const int P=13331;
ull p[N],h[N];
char s[N];
ull get(int l, int r){
return h[r]-h[l-1]*p[r-l+1];
}
void work(){
int n;cin>>n;
rep(i,1,n){
cin>>s[i];
}
p[0]=1;h[0]=0;
rep(i,1,n){
h[i]=h[i-1]*P+(s[i]-'a'+1);
p[i]=p[i-1]*P;
}
int q;cin>>q;
rep(i,1,q){
int s1,s2;
cin>>s1>>s2;
int l,r,mid;
l=0,r=n-s1;
while (l<=r){
mid=(l+r)/2;
if (get(s1,s1+mid)==get(s2,s2+mid)){
l=mid+1;
}
else{
r=mid-1;
}
}
cout<<l<<endl;
}
}
signed main(){
CIO;
work();
return 0;
}
进阶思路2:马拉车
思路:
p[i]表示以i为中心的字符串是回文的最大半径
1.预处理,字符串之间加上#,开头加上¥使得原来的字符串无论奇偶都变成奇数
2.一遍for处理以i为中心的字符串,暴力两边找,找完更新一个最大右边界(i+p[i]),同时记录此时的i,令他为id。
精髓:如果发现这个i在最大右边界的左边,就继承id左边对称的那个p[i]避免暴力查找。
注意:此时如果此时对称那一边的p[i]超出了id的边界,超出那一部分还要暴力。
大体思路就是这样,本质就是个记忆化,充分利用回文串对称的性质,避免重复暴力。
板子
void manacher(){
str[0]='$';str[1]='#';//初始化新字符串
for(int i=1;i<=len;i++){
str[i*2]=s[i];
str[i*2+1]='#';
}
len=len*2+2;
str[len]='#';
int id=0,mx=0;
for(int i=1;i<len;i++){
if(mx>i)
p[i]=min(p[2*id-i],mx-i);//记忆化
else
p[i]=1;
while(str[i-p[i]]==str[i+p[i]]) p[i]++;
if(p[i]+i>mx){//如果发现更靠右的盒子
mx=p[i]+i;
id=i;
}
}
}
进阶思路3:回文dp
思路:
传统回文dp转移思路很好想到:
i,j表示从i到j是不是字符串,是1,不是0.
dp[i][j]=dp[i+1][j-1]&&(s[i]==s[j])
根据题意不一样,比如如果要找到其中几个特殊的回文串而不是全部,还有一种跳跃dp,大体思路就是跳过不需要的,节省时间。
样例:
找到以k或f或c为结尾的回文串个数。
跳跃dp
#include<iostream>
#include<vector>
using namespace std;
vector <int> a[500010];
int main ( )
{
int n;
cin >> n;
string s;
cin >> s;
s=" "+ s;
long long ans1=0,ans2=0,ans3=0;
for(int i=1;i<=n;i++){
long long tmp_ans=1;
a[i].push_back(i);
if(s[i]==s[i-1]){
tmp_ans++;
a[i].push_back(i-1);
}
for(int j=0;j<a[i-1].size();j++){
if(a[i-1][j]-1>0&&s[a[i-1][j]-1]==s[i]){
tmp_ans++;
a[i].push_back(a[i-1][j]-1);
}
}
if(s[i]=='k'){
ans1+=tmp_ans;
}
else if(s[i]=='f'){
ans2+=tmp_ans;
}
else if(s[i]=='c'){
ans3+=tmp_ans;
}
}
cout << ans1 << " " << ans2 << " " << ans3 << endl;
}
进阶思路4:回文自动机
不是很懂,但板子好用:
struct PAM {
int size, last, r0, r1;
int trie[maxn][26], fail[maxn], len[maxn], cnt[maxn];
PAM() {
r0 = size++, r1 = size++; last = r1;
len[r0] = 0, fail[r0] = r1;
len[r1] = -1, fail[r1] = r1;
}
void insert(int ch, int idx) {
int u = last;
while (str[idx] != str[idx - len[u] - 1])u = fail[u];
if (!trie[u][ch]) {
int cur = ++size, v = fail[u];
len[cur] = len[u] + 2;
for (; str[idx] != str[idx - len[v] - 1]; v = fail[v]);
fail[cur] = trie[v][ch]; trie[u][ch] = cur;
cnt[cur] = cnt[fail[cur]] + 1;
}
last = trie[u][ch];
}
void build(char* str) {
int len = strlen(str);
for (int i = 0; i < len; i++)
insert(str[i] - 'a' + 1, i);
}
}pam;
转换思路:求最长回文串-->求回文串个数
用前面的思路求出最长回文串之后,肯定会获得一串以i为中心的最大字符串。
思路:
1.先处理二十六个字母出现的个数,用前缀预处理
2.从末尾到中心出现ch的个数,因为此时的末尾到中心是以i为中心的最大回文串的一半,
所以计算从中心到末尾ch有多少个,就是以i为中心,以ch为末尾的回文串,也就是个数。
样例还是:
找到以k或f或c为结尾的回文串个数。
这是套马拉车板子,之后得到最长回文串,然后用一下思路转换。
void work(){
cin>>len;
rep(i,1,len){
cin>>s[i];
}
manacher();
for (char ch:{'k','f','c'}){
rep(i,1,len){
sum[i]=sum[i-1]+(str[i]==ch);
}
int ans=0;
rep(i,2,len-1){
ans+=sum[i+p[i]-1]-sum[i-1];//i最大右边界-(左边界-1)
}
cout<<ans<<" ";
}
}
会的和不会的回文串都讲了,这些还解决不了的回文串题目,那就算了吧,摆烂XD