leetcode之最长回文子串
介绍的求解方法有以下5种,最后一种仅当拓展视野,和我一样感兴趣的小伙伴们,可自行或与我一起共同拓展。
如题所示:
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: “babad”
输出: “bab”
注意: “aba” 也是一个有效答案。
示例 2:
输入: “cbbd”
输出: “bb”
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-palindromic-substring
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
Talk is cheap,write code is real!
暴力求解
又称无脑求解,只要直到回文串的基本性质,几个循环一套,条件多一些,即可求之。
其思想是:
- 根据回文子串的定义,枚举所有长度大于等于 2 的子串,依次判断它们是否是回文;
- 检查所有可能性的回文串。
上图(纯手工绘制,不喜勿喷 😚)
是这个样子,循环的次数是这个回文串长度的平方,i
称为回文串的起始beg位置,j
则为回文串的终点end位置,j-i+1
则表示回文串的长度。当回文串的beg=end时,则两边需要向中间靠拢,进一步的甄别回文串的有效性。需要注意的是,形如’a’这种单个字符的也是回文串。
#include<string>
#include<iostream>
using std::string;
bool _charEqual(string& s,int left,int right)
{
while(left<right)//当left>=right时,就表示一个回文串。
{
if(s[left]!=s[right])
return false;
++left;
--right;
}
return true;
}
string longestPalindrome(string s)
{
if(s.size()<2)
return s;
int maxLen=0,beg;
for(int i=0;i<s.size()-1;++i)
{
for(int j=i+1;j<s.size();++j)
{
if(_charEqual(s,i,j)
{
int length=j-i+1;
lenght>maxLen?maxLen=length,beg=i?maxLen=maxLen;
}
}
}
return s.substring(beg,maxLen);
}
证明过程真的很完美,希望博友们对我进行指导。
暴力解法:对某个有效的索引进行缩小,在这个范围内进行两边判断相等。
证明过程:回文串是具有对称性的,也就是说假定一个回文字符串str。在中间的位置存在一个对称轴i,只要前提满足回文字符串,那么必然会存在str+i==str-i.
那么缩小范围内之后,在其对称轴是否可以进行优化呢?假设str存在2个索引Left,right。和一个对称轴i=(left+right)/2。left和right不断靠拢。满足str[left]==str[right]时,left和right靠拢。仅当条件为否时,停止靠拢。问题是:对称轴是可以求出的,是否可以把这两个位置进行“缩小”并在此对称轴处减少sum(对称轴中对应的循环次数)即(sum-left*2)次?其实这个现象就类似于字符串的叠臂现象。假定叠臂,设存在一个虚拟对称轴zi。假设在zi中的某一处不相等。那么str在以zi为对称轴的必然不是回文串。
时间复杂度O(N³),这里对字符串中的来来回回都进行了比较,即两层的嵌套循环,其次就是对回文串的验证,共3次。
空间复杂度O(1),只用到了一个临时变量。
中心扩散法
这种方法是为最后的Manacher算法作铺垫。
观察:
1)aba 2)aa 3)bb 4)abcba
诸如上述描述的回文串都有不同的特性,例如1) 4)这种都是奇数型回文串,剩余的为偶数型回文串。
如图:
通过图上简短的演示,会发现一个小细节,回文串可能是奇数型的,这种回文串的对称中心刚好是一个字符;而偶数型回文串它的对称中心则是“间隙”,如图:
由于存在偶数性和奇数型情况的中心扩散法,所以难免要对str以i为对称轴的位置进行2次循环。分别用来识别这两种型号的回文串。
//检查回文完整性
void checkHuiWen(char* str, int* left, int* right)//需要排除字符串中,单个字符成为回文串的现象
{
while (*left>-1&&*right<strlen(str)&&str[*left] == str[*right])
{
--*left;
++*right;
}
++*left;
--*right;
}
//获取长度
int GetLength(int left, int right)
{
return right - left + 1;
}
//
char * longestPalindrome(char * s){
//char* subStr = NULL;
int left, right;
int maxLen = 0, curLen = 0, maxLeftIndex=0, maxRightIndex;
for (int i = 0; i < strlen(s); i++)
{
//奇数型检验
left = i - 1;
right = i + 1;
checkHuiWen(s, &left, &right);
//长度计算:right-left-1
curLen = GetLength(left, right);
if(maxLen < curLen)
{
maxLen = curLen;
maxLeftIndex = left;
}
//偶属性检验
left = i;
right = i + 1;
checkHuiWen(s, &left, &right);
curLen = GetLength(left, right);
if(maxLen < curLen)
{
maxLen = curLen;
maxLeftIndex = left;
}
}
//char subStr[1024]={'\0'};
char* subStr=(char*)malloc(sizeof(char)*(maxLen+1));
memset(subStr,0,sizeof(char)*(maxLen+1));
strncpy(subStr, s + maxLeftIndex, maxLen);
return subStr;
}
针对这种的时间复杂度为O(n^2)
空间复杂度为O(1).
动态规划
由于单个字符也是一个回文串,那么我们可以建造一个二维数组dp[][],其中dp数组的对角线位置[0][0],[1][1],[2][2],[3][3],映射到str中是不是就是一个单独的字符了?由于单个字符为回文,所以设定对角线上的数据为true.
定理dp[i][j]=true,(i=j)
其次就是探讨回文结构的问题。
我们大家知道
- 如果一个字符串是一个回文,在两边相等的同时,内部其实也是相同的,并且关于一个中心字符对称(奇数型),偶数型则是关于一个轴吧,虚构的。这也就是说,只要确保内部相同,当前的2个字符相同,我们完全可以将上一结构的值赋值过来。即动态转移方程dp[i][j]=dp[i+1][j-1]。由于博主脑子慢,这个式子一下子没看出来里面的玄机。所以这次我将进行一个详解。
关于i,j。大家可以把他参照为在str中的两个哨兵。而i-1,j+1这个操作其实就相当于一个线性链接的过程,因为在最开始我们认为单个字符为回文,即代表靠近单个字符的两边字符如果相等的话,那么它就是一个回文(aba)【对应的下标为012】,那么在这里i=0,j=2.经过相加后i=j=1,我们可以把对角线上的值传递到dp[i][j]=dp[i+1][j-1],以此类推。 提示:关于索引的问题,我们只需要保证不发生错位,即i与j的位置发生变换
- 由此引出了下列的问题,关于偶数型回文串的判定。针对这个,我们需要注意在最初的结构(aa)【0,1】我们不能使用转移方程,否则的话就导致我们参考了【1,0】原有的顺序发生逆转。剩下的操作与上述相同。
- 还要强调一点,因为难免在一个字符串中出现相等的,但内部就不一定是字符串,所以要把上一结构值转移后,需要判断是否是字符串,如果是,我们才能更新长度以及起始索引。
char** GetMaxLength_HuiWen_SubString(char* str,int** flag,size_t length)
{
//开始填表
if (!strcmp(str, ""))
return NULL;
char* subStr = NULL;
//max=1,beg=0是为了防止字符串为普通字符串时,默认字符串中第一个字符为回文串。
int max = 1;
int beg =0;
for (size_t i = 1; i < length; i++)
{
for (size_t j = 0; j <i; j++)
{
//对角线有效性检验i+1,j-1。(索引是否发生错位)
if (str[i] == str[j])//确保在str上的i与j不错位。
{
//当i+1,j-1之后,满足i<=j时,参照flag[i+1][j-1]的值;若不满足,只要相等就将flag[i+1][j-1]的值拷贝。
int tempI = i - 1;
int tempJ = j + 1;
if (tempI >= tempJ)//验证索引是否错位。
{
/*
* 参照上一结构:若上一结构是0的话。当前索引所在字符相同的话,不足以证明是回文串
*/
flag[i][j] = flag[i - 1][j + 1];
//需要在这里进行回文串大小的获取。并保存最大长度
//维护最长回文子串的长度以及起始位置
int curLen = i-j + 1;
flag[i][j] == 1 && max < curLen ? max = curLen, beg = j : max = max;
}
else
{
flag[i][j] = 1;
//维护最长回文子串的长度以及起始位置
int curLen = i - j + 1;
flag[i][j] == 1 && max < curLen ? max = curLen, beg = j : max = max;
}
}
else
flag[i][j] = 0;
}
}
subStr = (char*)malloc(sizeof(char) * (max + 1));
memset(subStr, 0, sizeof(char) * (max + 1));
strncpy(subStr, str + beg, max);
std::cout << "填表如下:" << std::endl;
for (size_t i = 0; i < length; i++)
{
for (size_t j = 0; j < length; j++)
{
std::cout << flag[i][j] << " ";
}
std::cout << std::endl;
}
return &subStr;
}
int main()
{
char* str =const_cast<char*>( "ac");
//创建1个与str长度等维的矩形。
int** flag = (int**)malloc(sizeof(int) *(strlen(str)+1));
//实例化
for (size_t i = 0; i < LENGTH(str); i++)
flag[i] = (int*)malloc(sizeof(int) * (strlen(str) + 1));
for (size_t i = 0; i < LENGTH(str); i++)
for (size_t j = 0; j < LENGTH(str); j++)
{
if (i == j)
flag[i][j] = 1;//设置对角线即单个字符为true
else
flag[i][j] = 0;
}
char*sub=*(GetMaxLength_HuiWen_SubString(str, flag, strlen(str)));
std::cout << sub << std::endl;
free(sub);
sub = NULL;
}
时间复杂度:O(n^2) 涉及双重for循环
空间复杂度:O(n^2) 涉及二位数组。
心得:之前总是看到这样的dp[i][j]感到发懵,认为只能存数据,现在可以发现巧妙利用索引下标+题目的给的条件+要取得的最优值的特征。可以转换到二维数组里面,形成独特的解题方法。
Manacher(马拉车)算法
回文字符串存在两种类型:
- 偶数型
- 奇数型
该算法先对传来的字符串进行预处理:
字符串首尾加上一个任意字符比如(# @ 等),每个字符之间也加这样的字符。
比如:a —>#a# ;bb—>#b#b#。可以发现这两类字符都统一为奇数型回文串。
该算法主要是对中心扩散法进行了空间换时间的思想(动态规划),博主在这里卡就卡了很长时间。。。针对这种情况,首先,我们需要回文进行一个新的认识,也许只是仅仅对我而言,太笨了。回文不再是一个就如同链表一样一块接一块的如果读者也是这样认为的,那么可能在这个算法上会产生错觉 。 回文串会存在重叠,也就是说当前回文串的臂可能是另一个超长字符串的一部分。abacaba是一个例子。而此算法也就是在这个场景上,在一定程度上减少了扩散的步数。
所以这里涉及 变量:
- maxRight
- i
- center
- i’
- maxLeft
maxRight为当前最大回文串的右边界。center为当前最大回文串的中心。maxLeft是maxRight相对center来说的,i是当前进行扩散的区域,i’是i相对center对称过来的。
这里的动态规划的核心就是,在重叠这种情况下,减少重叠部分回文串的循环,以此来提高算法效率,即尽量能够参照对称点的回文串长度,将其照抄过来。下面为大家手动画一张图,帮助理解,这里以abacaba为例子。
这里只是大致演示了一下过程,首先要为每个字符准备一个dp[]用来存以当前为索引的字符串的长度,其次就是何时利用这个长度的问题了。
当对称点i’<maxRight-i时,不能采用最大的,为何,因为这样会把未被检验的字符排除在外。即我们要取最小值。
#include <iostream>
#include<string>
#include<vector>
#include<math.h>
using std::vector;
using std::string;
int getLen(string& str, int left, int right)
{
while (left >= 0 && right < str.length() && str[left] == str[right])
{
--left;
++right;
}
//由于预处理,扩散具有2种情况:
//1.当两个边界字符不相同时,肯定停留在了#字符上。
//2.可扩散时,两个有效字符一定相同。
return (right-left-1)/2;
}
int main()
{
string srcStr = "acewxsfaewqfguopjksgxhswahswzacewxsfaewqfguopjksgxhswahswzacewxsfaewqfguopjksgxhswahswzacewxsfaewqfguopjksgxhswahswzacewxsfaewqfguopjksgxhswahswzacewxsfaewqfguopjksgxhswahswzacewxsfaewqfguopjksgxhswahswzacewxsfaewqfguopjksgxhswahswzacewxsfaewqfguopjksgxhswahswzacewxsfaewqfguopjksgxhswahswzacewxsfaewqfguopjksgxhswahswzacewxsfaewqfguopjksgxhswahswzacewxsfaewqfguopjksgxhswahswzacewxsfaewqfguopjksgxhswahswzacewxsfaewqfguopjksgxhswahswzacewxsfaewqfguopjksgxhswahswzacewxsfaewqfguopjksgxhswahswzacewxsfaewqfguopjksgxhswahswzacewxsfaewqfguopjksgxhswahswzacewxsfaewqfguopjksgxhswahswzacewxsfaewqfguopjksgxhswahswzacewxsfaewqfguopjksgxhswahswzacewxsfaewqfguopjksgxhswahswzacewxsfaewqfguopjksgxhswahswzacewxsfaewqfguopjksgxhswahswzacewxsfaewqfguopjksgxhswahswzacewxsfaewqfguopjksgxhswahswzxxxsxxx";
//字符串预处理
string chStr = "#";
for (size_t i = 0; i < srcStr.length(); i++)
{
chStr += srcStr[i];
chStr += '#';
}
std::cout << chStr << std::endl;
int max = 1;
int start = 0;
int end = -1;
int right = -1;//当前索引可扩散的最右边界。
int center = 0;//中心位置。
vector<int> step;//之前走过的步长。
int curlen = 0;//curlen单臂长
for (int i = 0; i < (int)chStr.length(); i++)
{
/*
* 由于回文串存在重叠性,该算法的核心思想:
* Manacher算法巧妙的将两种回文类型(偶、奇)类型统一了。
* 设定了4个变量,分别为:maxRight, center,i,i'.
* maxRight:当前最大回文串的最大长度(右边界);
* center:当前最大回文串的中心
* ★i:对当前索引上的字符进行回文扩散;
* 首先,center是关于str(回文串对称的),在i尚未找到比之前center所在回文串对称区域的较大的回文串时,center不变。并且maxRight与center同理。仅当i<maxRight时,i完全可以参照对称点i'的跨度,然后将其复制过来,进行扩散。当然对称点i'的长度不能时时刻刻照搬的。maxRight-i的距离要严格≤maxLeft-i',只有这样,我们可以照搬,至少确定了在maxRight-i这段距离内是确定的。反之,照搬会漏掉一定的检验,从而导致结果不正确。
* 上面的证明存在了回文相对的一些重叠性。需要考虑。
* i'是i关于center对称的点。
*
*
*/
if (i <= right)
{
int a = 2 * center - i;
int minStep = step[a] < right - i ? step[a] : right - i;
curlen = getLen(chStr, i - minStep, i + minStep);
}
//curlen该回文串包含了#。/2有2个含义:1.得到单臂长;2.得到回文串长
else
{
curlen = getLen(chStr, i,i);
}
//i是关于当前(仅在回文串下有效)的中心索引(对称索引)
step.push_back(curlen);
if (i + curlen >= right)
{
center = i;//设定center为当前最大回文子串的索引。
right = i + curlen;//定位当前最大回文子串的右索引。
//!!!
}
if ((curlen << 1) + 1 > end - start)
{
start = i - curlen;
end = i + curlen;
}
}
string dest;
for (int i = start; i <=end; i++)
{
if (chStr[i] != '#')
dest += chStr[i];
}
std::cout << dest << std::endl;
std::cin.get();
}
心得体会
关于对动态规划dp数组的深入理解,要根据题意的要求,找到最优结构与最优子结构的关系,然后再综合的观察下标变换与最优子结构的关系。最后就是要相对完整的刻画一个较优的子结构,以及这个子结构会出现的种种情况来进行讨论。