Bootstrap

回文串求解的基础+进阶方法 (详细汇总)

本博客用于对回文串有一定基础的人,而不是教学。(因为写的不是很具体,更多的是给个大局思维,新人可能看不懂qwq)

应该是我写过目前最长的博客了(码字量应该是)

目录

求一串字符串中的回文串的个数。

基础思路:暴力

进阶思路1:哈希二分

进阶思路2:马拉车

进阶思路3:回文dp

找到以k或f或c为结尾的回文串个数。

进阶思路4:回文自动机

转换思路:求最长回文串-->求回文串个数

找到以k或f或c为结尾的回文串个数。


首先是给出一个例子:

求一串字符串中的回文串的个数。

基础思路:暴力

拿到这道题我们知道暴力求解回文串复杂度是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

;