回文子串(Manacher‘s Algorithm马拉车算法)以及回文子序列

前言:

我们先了解回文串,顾名思义就是前后翻转都一样,这样的字符串就是回文串,如aba,abba等

判断回文串的方法
  • 方法一:
    • 最简单的方法就是用原来的字符串跟翻转后的字符串进行比较,只要有一对字符不相等,那么就不是回文串。

代码如下:

# include <iostream>
# include <string>
# include <algorithm>
using namespace std;
int main(void)
{
	string s;
	cin>>s;
	string t = s;
	reverse(t.begin(),t.end());
	
	if(t==s) cout<<"Yes"<<endl;
	else cout<<"No"<<endl;
	return 0;
 } 
  • 方法二:
    • 利用栈来解决,但是这里要注意奇偶问题,也就是回文串的个数的总数是奇数还是偶数,这里要判断一下。
    • 假设len为字符串的个数,如果(len&1),那么我们就需要往栈中添加前1到len/2个元素,然后弹栈进行检查,假设压栈的最后一个元素的下标为x,那么我们就拿字符串x+2到len的子串进行比较即可(因为x+1这个元素是最中间的,不需要比较)
    • 如果len是偶数,那么我们就需要往栈中添加前1到len/2个元素,然后弹栈进行检查,假设压栈的最后一个元素的下标为x,那么我们就拿字符串x+1到len的子串进行比较即可,举个例子:len为8,那么入栈的就有1到4,然后我们拿4到8进行比较。

代码如下:

# include <iostream>
# include <string>
# include <algorithm>
# include <stack>
using namespace std;
int main(void)
{
	string s;
	cin>>s;
	stack<char> q;
	int len = s.size();
	for(int i=0;i<len/2;++i){
		q.push(s[i]);
	}
	
	int k = len/2;
	if(len&1) k = len/2 + 1;
	
	while(q.size()){
		if(q.top()!=s[k]){
			cout<<"No"<<endl;
			return 0;
		}
		q.pop();
		++k;
	}
	cout<<"Yes"<<endl;
	return 0;
 } 
  • 方法三:
    • 这也是我最喜欢的做法,用双指针(请点击,了解双指针。)来解决,设置两个变量,i 在头,j 在尾,代表下标,然后一前一后进行遍历,只要发现s[i]!=s[j],我们就认为此字符串不是回文串。(这里不需要判断长度是否为奇偶,因为我们循环的条件为while(i<j)

代码如下:

# include <iostream>
# include <string>
# include <algorithm>
# include <stack>
using namespace std;
int main(void)
{
	string s;
	cin>>s;
	int i=0,j=s.size()-1;
	while(i<j){
		if(s[i]!=s[j]){
			cout<<"No"<<endl;
			return 0;
		}
		++i,--j;
	}
	cout<<"Yes"<<endl;
	return 0;
 } 


回文子串

概念:具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串,然后此子串顺序遍历与逆序遍历得到的结果是一样的.

判断回文子串的方法:

在这里插入图片描述

  • 方法一:

采用DP求解

1.首先需要理解回文子串的含义:
如果一个字符串前后两端对应的子串不相同,那么该字符串一定不是回文子串。
当首尾两端字符相同时,然后比较d[i + 1][j - 1]是否是回文子串。

2.按照单个字符、两个字符和多个字符等三个条件依次进行判断。得出动态规划方程为 :
a.单个字符: j == i and s[i] == s[j] => dp[i][j] = true;
b.两个字符: j - i == 1 and s[i] == s[j] => dp[i][j] = true;
c.多个字符: j - i > 1 and dp[i][j] = dp[i + 1][j - 1] and s[i] == s[j]

代码如下:

class Solution {
public:
    int countSubstrings(string s) {
        int dp[1010][1010]={0};
        int ans = 0;
        for(int i=0;i<s.size();++i){
            for(int j=0;j<=i;++j){
                int st  = 0;
                if(i==j && s[i]==s[j])
                    dp[j][i]=1;
                else if(j+1==i && s[i]==s[j])
                    dp[j][i] =1;
                else if(i-j>=2 && s[i]==s[j] &&  dp[j+1][i-1])
                    dp[j][i] = 1;
                else st = 1;
                if(!st) ++ans;
            }
        }
        return ans;
    }
};
  • 方法二:

采用中心扩展法,简单来讲:

就是 把字符串中的 一个或者相邻两个字符 当作中心,
然后通过两个指针 分别向左向右 扩展,并在扩展的过程中记录当前时刻是否具有 回文属性
注意:必须把 当前元素为中心元素 和 当前元素与后一个元素同为中心元素 两种情况都考虑到

class Solution {
public:
    int func(string& s,int l,int r){
        int ans = 0;
        while(l>=0 && r<s.size() && s[l]==s[r]){
            --l;++r;
            ++ans;
        }
        return  ans;
    }
    int countSubstrings(string s) {
        int ans = 0;

        for(int i=0;i<s.size();++i){
            ans+=func(s,i,i);
            ans+=func(s,i,i+1);
        }
        return ans;
        
    }
};
  • 方法三:
    采用马拉车(Manacher’s Algorithm)算法,在下文会详细说到,建议先看完下面的详解再倒回来看代码。
class Solution {
public:
    int countSubstrings(string s) {
        string t="$#";
        for(auto &i:s){
            t.push_back(i);
            t.push_back('#');
        }
        t+='^';
        

        int n = s.size();
        int ans = 0;

        vector<int> q(t.size(),0);
        int right = 0,mi = 0;
        for(int i=1;i<t.size()-1;++i){
            q[i] = right>i ? min(q[2*mi-i],right-i) : 1;
            while(t[i+q[i]]==t[i-q[i]])  ++q[i];
            if(i>right){
                mi = i;
                right = i;
            }

            ans+=q[i]/2;
        }
        //for(auto i:q) cout<<i<<' ';
        return ans;
    }
};


回文子串返回最长子串

在这里插入图片描述

思路:
这道题
但是要特别注意长度为1或者2的情况,具体看代码。

class Solution {
public:
    bool dp[1010][1010];
    string longestPalindrome(string s) {
        int idx=0,idy=0;
        int ans = 0;
        int len = s.size();
        for(int i=0;i<len;++i){   
            dp[i][i] = 1;
        }

        for(int i=len-1;i>=0;--i){
            for(int j=i;j<len;++j){

                if(s[i]==s[j]) //这里分为两种情况,要么长度小于等于2,要么就跟dp[i+1][j-1]有关
                    dp[i][j] = j-i<2 ? 1 : dp[i+1][j-1];

                if(dp[i][j] && ans<j-i+1){
                    ans = j-i+1;
                    idx = i,idy = j;
                }
            }
        }
        return s.substr(idx,ans);
    }
};

Manacher‘s Algorithm(马拉车算法)

用来求最长回文子串

我们都知道,在处理回文串的时候要考虑奇偶问题,但是用马拉车算法写就不用考虑这个问题,因为它第一步就是把字符串长度恒改为奇数

  • 比如"bab"是奇数形式的回文,"naan"就是偶数形式的回文,马拉车算法的第一步是预处理,做法是在每一个字符的左右都加上一个特殊字符,比如加上’#’,那么
  • bab --> #b#a#b#
  • naan --> #n#a#a#n#
  • 这样做的好处是不论原字符串是奇数还是偶数个,处理之后得到的字符串的个数都是奇数个

一般我们会再在开头和结尾各添加一个不相等的字符,如“@#a#b#c#^”这样

在马拉车算法中,我们格外要弄懂一件事,那就是p[ ]数组的含义,p[i]表示以字符串S[ i ]字符为中心的回文子串的半径,表示以字符S[ i ]为中心的最长回文字串的最端右字符到T[i]的长度,如以S[ i ]为中心的最长回文子串的为S[ l, r ],那么P[ i ]=r-i+1。这样最后遍历数组P[ ],取其中最大值即可。若P[ i ]=1表示该回文串就是T[ i ]本身。

举个例子:
在这里插入图片描述

这里,P[ i ]-1就是该回文子串在原字符串S中的长度 ,那就是P[i]-1就是该回文子串在原字符串S中的长度

证明:

  • 因为我们改造后的字符串是原来长度的两倍+1,增加了len+1个‘#’字符,那么对于以T[i]为中心的最长回文字串,其长度就为2*P[i]-1,

  • T中所有的回文子串,其中分隔符‘#’的数量一定比其他字符的数量多1,也就是有P[i]个分隔符,那么可想而知剩下P[i]-1个字符来自原字符串

难点就是怎么求P[ ]数组

从左往右计算数组P[ ], 我们用变量Mi为之前取得最大回文串的中心位置,而变量R是最大回文串能到达的最右端的值。

  • 当 i <=R时,如何计算 P[ i ]的值了?毫无疑问的是数组P中点 i 之前点对应的值都已经计算出来了。利用回文串的特性,我们找到点 i 关于 Mi 的对称点 j ,其值为 j= 2*Mi-i 。因,点 j 、i 在以Mi 为中心的最大回文串的范围内([L ,R])

    • a)那么如果P[j] <R-i (同样是L和j 之间的距离),说明,以点 j 为中心的回文串没有超出范围[L ,R],由回文串的特性可知,从左右两端向Mi遍历,两端对应的字符都是相等的。所以P[ j ]=P[ i ](这里得先从点j转到点i 的情况),如下图:
      在这里插入图片描述

    • b)如果P[ j ]>=R-i (即 j 为中心的回文串的最左端超过 L),如下图所示。即,以点 j为中心的最大回文串的范围已经超出了范围[L ,R] ,这种情况,等式P[ j ]=P[ i ]还成立吗?显然不总是成立的!因,以点 j 为中心的回文串的最左端超过L,那么在[ L, j ]之间的字符肯定能在( j, Mi ]找到相等的,由回文串的特性可知,P[ i ] 至少等于R- i,至于是否大于R-i(图中红色的部分),我们还要从R+1开始一一的匹配,直达失配为止,从而更新R和对应的Mi以及P[ i ]。
      在这里插入图片描述

  • 2)当 i > R时,如下图。这种情况,没法利用到回文串的特性,只能老老实实的一步步去匹配。
    在这里插入图片描述

string Manacher(string s)
{
    /*改造字符串*/
    string res="$#";
    for(int i=0;i<s.size();++i)
    {
        res+=s[i];
        res+="#";
    }
  res=res+"^";
    /*数组*/
    vector<int> P(res.size(),0);
    int mi=0,right=0;   //mi为最大回文串对应的中心点,right为该回文串能达到的最右端的值
    int maxLen=0,maxPoint=0;    //maxLen为最大回文串的长度,maxPoint为记录中心点

    for(int i=1;i<res.size()-1;++i)
    {
        P[i]=right>i ?min(P[2*mi-i],right-i):1;     //关键句,文中对这句以详细讲解
        
        while(res[i+P[i]]==res[i-P[i]])
            ++P[i];
        
        if(right<i+P[i])    //超过之前的最右端,则改变中心点和对应的最右端
        {
            right=i+P[i];
            mi=i;
        }

        if(maxLen<P[i])     //更新最大回文串的长度,并记下此时的点
        {
            maxLen=P[i];
            maxPoint=i;
        }
    }
    return s.substr((maxPoint-maxLen)/2,maxLen-1);
}

最长回文子序列

在这里插入图片描述
思路:首先定个dp数组,其含义是在子串s[i…j]中,最长回文子序列的长度为dp[i][j].
那么我们就可以分情况讨论,当s[i]==s[j]时,则dp[i][j] = dp[i+1][j-1]+2;若不相等,那么其长度一定是在(i+1,j)或者(i,j-1)这两个区间选择。
所有dp问题,一定要记得是否需要对数组进行初始化,这个很重要!
这里需要dp[x][x]进行全部赋值为1,因为s[x] == s[x]这是必然的!还有当i>j时,dp[i][j]应该赋值为0;
在这里插入图片描述

class Solution {
public:
    int longestPalindromeSubseq(string s) {
        int n = s.size();
        vector<vector<int> > dp(n,vector<int>(n,0));

        for(int i=0;i<n;++i){
            dp[i][i] = 1;
        }

        for(int i=n-1;i>=0;--i){
            for(int j=i+1;j<n;++j){
                if(s[i]==s[j]){
                    dp[i][j] = dp[i+1][j-1]+2;
                }else{
                    dp[i][j] = max(dp[i+1][j],dp[i][j-1]);
                }
            }
        }
        return dp[0][n-1];
    }
};

第二种做法是利用最长公共子序列来解决。(这个方法比较低效)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值