最长的回文子串(四种方法)

早过忘川 提交于 2019-12-09 21:39:28

本文是观看了很多博客,最后整理而成……

  • 回文串:“回文串”是一个正读和反读都一样的字符串,比如“level”或者“noon”等等就是回文串。

手动判断该字符串是否为回文串:

#include <iostream>
using namespace std;
int main(){
    string str;
    int i, j;
    while (cin >> str){   
	    int flag = 1;
        for (i = 0,j = str.length()-1; i <= j; i++,j--){
            if (str[i] != str[j]){
                flag = 0;
                break;
            }
        }
        if (flag)
            cout << "YES" << endl;
        else
            cout << "NO" << endl;
    }
    return 0;
}

 利用函数判断给字符串是否为回文串:

#include<iostream>
#include<algorithm>
using namespace std;
int hui(string s){
	string s1 = s;
	reverse(s.begin(), s.end());
	return s==s1;
}
int main(){
	string s;
	cin >> s;
	if(hui(s))  cout << "yes";
	else   cout << "no";
	return 0;
}
  • 回文子串在原串中连续出现的字符串片段

找出最长的回文子串

方法一: 暴力求解

把该字符串的所有子串列举出来,分别判断子串是否为回文串,找出最长的那个子串;

列举所有的回文子串时间复杂度为O(n^2),判断是否为回文时间复杂度为O(n),两者循环相套,故最终复杂度为 O(n^3),此方法时间复杂度过大,容易超时,不建议使用;

#include<iostream>
#include<algorithm>
using namespace std;
int hui(string s){
    string s1 = s;
    reverse(s.begin(), s.end());
    return s1 == s? 1 : 0;
}
int main(){
    string s, m;
    int t;
    while(!(cin >> s).eof()){
        int maxn = 0;
        for(int i = 0; i < s.length(); i++){
            for(int j = 1; j+i <= s.length(); j++){
                m = s.substr(i, j);
                if(hui(m)){
                    t = m.length();
                    if(maxn < t){
                        maxn = t;
                    }
                }
            }
        }
        cout << maxn << endl;
    }
    return 0;
}

方法二:动态规划法

令 dp[i][j] 表示 S[i] 至 S[j] 所表示的子串是否为回文子串,是则为1,不是为0。如此根据 S[i] 是否等于 S[j],可以把问题分为两类:

(1)S[i] == S[j],那么只要 S[i+1] 至 S[j-1] 是回文子串,S[i] 至 S[j] 就是回文子串;如果 S[i+1] 至 S[j-1] 是不是回文子串,则 S[i] 至 S[j]也不是回文子串。

(2) S[i] != S[j],那么 S[i] 至 S[j] 一定不是回文子串。  

由此可以写出其状态转移方程:

边界:dp[i][i] = 1,dp[i][i+1] = (S[i]==S[i+1] ? 0 : 1)       ps:这里的dp初始化记录了长度为1和2的回文子串

但是这里还存在一个问题,就是在求 dp[i][j] 时,无法保证 dp[i+1][j-1] 已经被计算了,比如先固定 i=0,然后 j 从 2 开始枚举。当求解dp[0][2] 时,dp[1][1]已经在初始中得到;当求解dp[0][3]时,会转换求dp[1][2],而dp[1][2]也在初始化中获得;当求解dp[0][4]是,转换求解dp[1][3],但dp[1][3]之前却没有被计算出来,因此无法转移状态。

根据上面的公式,边界的长度表示长度为1和2的回文子串,且每次转移时都说对子串的长度减1。因此不妨按照子串的长度和子串的初始位置进行枚举,即第一次可以枚举长度为3的子串的dp值,第二次在第一次的基础上枚举长度为4的子串的dp值....直到枚举到原字符串的长度。

时间复杂度为 O(n^2);

根据分析,写出如下代码:

#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
const int M=1010;
int dp[M][M];  
int main(){
	int i,j,k,s,e,ans=1; 
	string str;
	cin>>str;
	int len=str.length();
	for(i=0;i<len;i++){
		dp[i][i]=1;
		if(str[i]==str[i+1]){
			dp[i][i+1]=1;
			ans=2;
		}
	}
	for(k=3;k<=len;k++){ //子串的长度 
		for(i=0;i+k-1<len;i++){ //子串左端点 
			j=i+k-1; //子串右端点
			if(str[i]==str[j]&&dp[i+1][j-1]){
				dp[i][j]=1;
				ans=k;
				s=i;e=j; //保存最长回文子串的下标
			}
		} 
	}
	for(i=s;i<=e;i++){
		cout<<str[i]; 
	}
	cout<<endl<<ans<<endl;	
}

方法三:中心扩展

回文串其实就是关于中心对称的,回文中心的两侧互为镜像,因此,回文可以从它的中心展开,并且只有 2n-1 个这样的中心;

解释: 2n - 1

(1)当回文的中心为双数时:如 abba ,可以划分为 ab bb ba,对于n长度的字符串,这样的划分有 n-1 种。

(2)当回文的中心为单数时,如 abcd ,可以划分为 a b c d, 对于n长度的字符串,这样的划分有 n 种。

比如有字符串 aaabcbabb,这时最长回文子串是 abcba,中心是c;又有字符串aaadccdabcd,这时最长回文子串是 adccda,中心是cc。 由此可见中心点既有可能是一个字符,也有可能是两个字符,当中心为一个字符的时候有 n 个中心,当中心为两个字符的时候有 n-1 个中心,所以一共有 2n-1 个中心。 然后for循环开始从左到右遍历,为什么会有两次 expandAroundCenter,一次是 i 和 i 本身,一次是 i 和 i+1,这就是上面说到的一个中心与两个中心。 而后会去判断这两种情况下谁的回文子串最长,并标记出这个子串在原字符串中的定位,即 start 和 end。

public String longestPalindrome(String s) {
    if (s == null || s.length() < 1) return "";
    int start = 0, end = 0;
    for (int i = 0; i < s.length(); i++) {
        int len1 = expandAroundCenter(s, i, i);
        int len2 = expandAroundCenter(s, i, i + 1);
        int len = Math.max(len1, len2);
        if (len > end - start) {
            start = i - (len - 1) / 2;
            end = i + len / 2;
        }
    }
    return s.substring(start, end + 1);
}

private int expandAroundCenter(String s, int left, int right) {
    int L = left, R = right;
    while (L >= 0 && R < s.length() && s.charAt(L) == s.charAt(R)) {
        L--;
        R++;
    }
    return R - L - 1;
}

下面代码是先找一个中心的回文串,再找两个中心的回文串; 

#include<iostream>
#include<cstring>
using namespace std;
string longest(string &s){
    const int len = s.size();
    int maxlen = 1;
    int start = 0;
    for(int i = 0; i < len; i++){//求长度为奇数的回文串
        int j = i - 1, k = i + 1;
        while(j >= 0 && k < len && s.at(j) == s.at(k)){
            if(k - j + 1 > maxlen){
                maxlen = k - j + 1;
                start = j;
            }
            j--;
            k++;
        }
    }
    for(int i = 0; i < len; i++){//求长度为偶数的回文串
        int j = i, k = i + 1;
        while(j >= 0 && k < len && s.at(j) == s.at(k)){
            if(k - j + 1 > maxlen)
            {
                maxlen = k - j + 1;
                start = j;
            }
            j--;
            k++;
        }
    }
     return s.substr(start, maxlen);
}
int main()
{
    string s;
    cin >> s;
    int n = longest(s).size();
    cout << longest(s) << endl;
    cout << n;
    return 0;
}

方法四:Manacher 法(俗称马拉车算法)

马拉车算法Manacher‘s Algorithm是用来查找一个字符串的最长回文子串线性方法,由一个叫Manacher的人在1975年发明的,这个方法的最大贡献是在于将时间复杂度提升到了线性,即 O(n),这是非常了不起的。

【Manacher算法原理及实现过程】

为啥会有这个马拉车算法,我们知道回文子串的判定和长度的奇偶性是有关系的,由于回文分为偶回文(比如 bccb)和奇回文(比如 bcacb),而在处理奇偶问题上会比较繁琐,所以这里我们使用一个技巧,在字符间插入一个字符(前提这个字符未出现在串里),常用的是"$""#"。

由于回文串的长度可奇可偶,比如 "bob" 是奇数形式的回文,"noon" 就是偶数形式的回文,马拉车算法的第一步预处理,做法是在每一个字符的左右都加上一个特殊字符,比如加上'#',那么

bob    -->    #b#o#b#

noon    -->    #n#o#o#n# 

这样做的好处是不论原字符串是奇数还是偶数个,处理之后得到的字符串的个数都是奇数个,这样就不用分情况讨论了,而可以一起搞定。

Len数组的意义及性质

接下来我们还需要和处理后的字符串 s_new[i] 等长的数组 Len,其中 Len[i] 表示以 s_new[i] 字符为中心的最长回文字串的最右字符到s_new[i]的长度(可以看成是回文子串的半径,最右边到中心点的距离)假设最右的元素下标为r,那么Len[i]=r-i+1。若 s_new[i] = 1,则该回文子串就是 s_new[i] 本身,那么我们来看一个简单的例子:

(1)# 1 # 2 # 2 # 1 # 2 # 2 #
         1 2 1 2 5 2 1 6 1 2 3 2 1

(2)

Len数组有一个性质那就是 Len[i]-1 就是该回文子串在原字符串 s 中的长度。

证明:

看上面那个例子,以中间的 '1' 为中心的回文子串 "#2#2#1#2#2#" 的半径是6,而未添加#号的回文子串为 "22122",长度是5,为半径减1。这是个普遍的规律么?我们再看看之前的那个 "#b#o#b#",我们很容易看出来以中间的 'o' 为中心的回文串的半径是4,而 "bob"的长度是3,符合规律。再来看偶数个的情况"noon",添加井号后的回文串为 "#n#o#o#n#",以最中间的 '#' 为中心的回文串的半径是5,而 "noon" 的长度是4,完美符合规律。所以我们只要找到了最大的半径,就知道最长的回文子串的字符个数了。只知道长度无法确定子串,我们还需要知道子串的起始位置。

首先在转换得到的字符串str中,所有的回文字串的长度都为奇数,那么对于以s_new[i]为中心的最长回文字串,其长度就为2*Len[i]-1,经过观察可知,s_new中所有的回文子串,其中分隔符的数量一定比其他字符的数量多1,也就是有Len[i]个分隔符,剩下Len[i]-1个字符来自原字符串,所以该回文串在原字符串中的长度就为Len[i]-1。

有了这个性质之后,那么最长回文子串问题就转化为求所有的 Len[i] 的最大值问题 

我们还是先来看中间的 '1' 在字符串 "#1#2#2#1#2#2#" 中的位置是7,而半径是6,貌似7-6=1,刚好就是回文子串 "22122" 在原串 "122122" 中的起始位置1。那么我们再来验证下 "bob","o" 在 "#b#o#b#" 中的位置是3,但是半径是4,这一减成负的了,肯定不对。所以我们应该至少把中心位置向后移动一位,才能为0啊,那么我们就需要在前面增加一个字符,这个字符不能是#号,也不能是s中可能出现的字符,所以我们暂且就用美元号 $ 吧。这样都不相同的话就不会改变p值了,那么末尾要不要对应的也添加呢,其实不用的,不用加的原因是字符串的结尾标识为'\0',等于默认加过了。那此时 "o" 在 "$#b#o#b#" 中的位置是4,半径是4,一减就是0了,貌似没啥问题。我们再来验证一下那个数字串,中间的 '1' 在字符串 "$#1#2#2#1#2#2#" 中的位置是8,而半径是6,这一减就是2了,而我们需要的1,所以我们要除以2。之前的 "bob" 因为相减已经是0了,除以2还是0,没有问题。再来验证一下 "noon",中间的 '#' 在字符串 "$#n#o#o#n#" 中的位置是5,半径也是5,相减并除以2还是0,完美。可以任意试试其他的例子,都是符合这个规律的,最长子串的长度是半径减1,起始位置是中间位置减去半径再除以2。

那么下面我们就来看如何求Len数组,需要新增两个辅助变量 mx 和 id,其中 id 为能延伸到最右端的位置的那个回文子串的中心点位置,mx 是回文串能延伸到的最右端的位置,这个算法的最核心的一行如下:

p[i] = mx > i ? min(p[2 * id - i], mx - i) : 1;

mx - i > P[j] 的时候,以S[j]为中心的回文子串包含在以S[id]为中心的回文子串中,由于 i 和 j 对称,以S[i]为中心的回文子串必然包含在以S[id]为中心的回文子串中,所以必有 P[i] = P[j],见下图:
 

 当 P[j] >= mx - i 的时候,以S[j]为中心的回文子串不一定完全包含于以S[id]为中心的回文子串中,但是基于对称性可知,下图中两个绿框所包围的部分是相同的,也就是说以S[i]为中心的回文子串,其向右至少会扩张到mx的位置,也就是说 P[i] = mx - i。至于mx之后的部分是否对称,就只能老老实实去匹配了。

对于 mx <= i 的情况,无法对 P[i]做更多的假设,只能P[i] = 1,然后再去匹配了。

#include <iostream>                                
#include <string>
#include <cstring>
#include <algorithm>
using namespace std;
int n,len,len_new;
string s,s_new;
int Len[10000] = {0};
void init(string s) {
	s_new.resize(2*len+5);
	s_new[0] = '#';
	for(int i = 1; i <= len; i++) {
		s_new[2*i-1] = s[i-1];
		s_new[2*i] = '#';
	}
	s_new[2*len] = '#';
}
int manacher() {
	int ans = -1; 
	int id=0,mx=0;
	Len[0] = 1;
	Len[len_new-1] = 1;
	for(int i = 1; i < len_new-1; i++) {
		int j = 2*id-i;
		if(s_new[i] + Len[j] < mx)
			Len[j] = min(mx-1,Len[j]);
		else
			Len[i] = 1;
		while(s_new[i - Len[i]] == s_new[i + Len[i]])   Len[i]++;
		if(Len[i] + i > mx) {
			mx = Len[i]+i;
			id = i;
		}
	}
	for(int i = 0; i < len_new; i++)
		ans = max(ans,Len[i]-1);
	return ans;
}
int main() {
	cin>>s;
	len = s.length();
	init(s);
	len_new = 2*len+1;
	cout<<"s_new = "<<s_new;
	cout<<"\nres = "<<manacher()<<"\n";
	for(int i = 0 ; i < len_new; i++) {
		cout<<s_new[i]<<" ";
	}
	cout<<"\n";
	for(int i = 0 ; i < len_new; i++) {
		cout<<Len[i]<<" ";
	}
	return 0;
}

#include <bits/stdc++.h>
using namespace std;
const int maxn =1e6;
string str;
string s_new;
int len[maxn<<1];
int init(string st)
{
    int len = st.size();
    s_new='$';
    for(int i =1; i <= 2*len; i+=2)
    {
        s_new += '#';
        s_new += st[i/2];
    }
    s_new+='#';
    s_new+='\0';
    return 2*len+1;// 返回 s_new 的长度
 
}
int Manacher(string st,int len_)
{
    int mx = 0,ans = 0,po =0;//mx即为当前计算回文串最右边字符的最大值
    for(int i =1; i <= len_ ; i++)
    {
        if(mx>i)
            len[i]=min(mx-i,len[2*po-i]);
        else
            len[i]=1;//如果i>=mx,要从头开始匹配
        while(st[i-len[i]]==st[i+len[i]])
            len[i]++;
        if(len[i]+i>mx)//若新计算的回文串右端点位置大于mx,要更新po和mx的值
        {
            mx = len[i]+i;
            po = i;
        }
        ans = max(ans,len[i]);//返回Len[i]中的最大值-1即为原串的最长回文子串额长度
    }
    return ans  - 1;
}
int main(){
	string s;
	cin >> s;
	cout << Manacher(s_new, init(s));
	return 0;
}

 

 

 

 

 

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!