最长回文子串
1. 题目描述:
给定一个字符串 s
,找到 s
中最长的回文子串。你可以假设 s
的最大长度为 1000。
示例1:
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
示例2:
输入: "cbbd"
输出: "bb"
2. 题目分析:
leetcode
的官方题解写得挺好的,以下就参考它的一些做法来写一下。
以下的几种方法多多少少都是利用了回文的对称性。
-
暴力法
暴力法的思路是比较简单的,主要是提取出每一个子串,类似于冒泡那样提取,然后再看每一个子串是不是回文,如果是回文,那就看它的长度是不是比已知的回文长度要长,是的话就更新回文长度最大值、起始点、终止点。总体来讲还是比较简单的,下面给出检测是否是回文的函数,利用回文的对称性。
public boolean isPlalindrome(String s) { int len = s.length(); for(int i = 0; i < len / 2; i++) { if(s.charAt(i) != s.charAt(len - 1 - i)) { return false; } } return true; }
-
中心扩展算法
回文的一个很重要的特性就是对称性,所以回文中心的两边是相等的,从中心扩展开,每一个字符串都可能有
2n - 1
个中心点,比如“baabcd”
以单个字符为中心就有n个,以两个字符为中心就是aa
,就有n - 1
个。//中心扩展函数 public int center_expand(String s, int left, int right){ while(left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)){ left--; //向左边扩展 right++; //向右边扩展 } int len = right - left - 1; //记录长度 return len; } //查找最长回文子串 public String longestPalindrome(String s) { int len = s.length(); if(len <= 1) return s; //字符串为空或者长度等于1,直接返回 int max = 0; //记录字符串最大值 String ret = null; //存放回文 for(int i = 0; i < len; i++){ int len1 = center_expand(s, i, i); //以i为中心,i为左,i也为右 int len2 = center_expand(s, i, i + 1); //以(i, i + 1)为中心,i为左,i+1为右 int len3 = Math.max(len1, len2); //取出最大长度 if(len3 > max){ int L = i - (len3 - 1) / 2; //最大回文长度的左边 int R = i + len3 / 2; //最大回文长度的右边 max = len3; //回文的最大长度 ret = s.substring(L, R + 1); //将回文存放在ret中 } } return ret; }
-
动态规划算法 (看这个大神写的)
动态规划算法两个步骤:
- 定义“状态”。
- 列出状态转换方程。
1、定义 “状态”,这里 “状态”是一个布尔型二维数组。
dp[l, r]
表示子串s[l, r]
(包括区间左右端点)是否构成回文串,是一个二维布尔型数组。即如果子串s[l, r]
是回文串,那么dp[l, r]
= true。2、状态转移方程:
dp[l, r] = (s[l] == s[r] and (r - l <= 2 or dp[l + 1, r - 1]))
首先,
s[l, r]
子串是回文的话,字符s[l] 必须等于 s[r]
,即最左端 == 最右端,利用了回文的对称性。其次,如果字符串长度比较短,比如
aba
或者aa
,既然s[l] == s[r]
都是a
,那就不用再多想了,这个一定是回文。这个短字符串满足r - l <= 2
。再者,如果字符串比较长呢,不满足
r - l <= 2
,比如abba
,已知s[l] == s[r] == a
还不行,还需要dp[l + 1, r - 1] = true成立
。具体该怎么循环,看哪个先有,哪个后有,是先有
dp[l + 1, r - 1]
,再有dp[l, r]
,所以所以这次的循环是 r 不动,l 由上往下动。两个循环,相当于冒泡的写法,可以遍历每一个子串的起止点,为每一个子串编写状态,看下图。public static String longestPalindrome(String s) { int len = s.length(); if(len <= 1) return s; //字符串为空或者长度等于1,直接返回 boolean[][] dp = new boolean[len][len]; //记录每一个子串的状态,dp[i][j]=true表明,以i为起点,j为终点的子串是回文 int max = 0; //最大回文长度 String ret = s.substring(0, 1); //存放回文,初始化为s的第一个字符,假如该字符串没有回文,那就直接返回字符串的第一个字符 for(int r = 1; r < len; r++){ for(int l = 0; l < r; l++){ //这两个循环很关键, if(s.charAt(r) == s.charAt(l) && (r - l <= 2 || dp[l+1][r-1])){ dp[l][r] = true; if(max < r - l + 1) { max = r - l + 1; //max值更新 ret = new String(s.substring(l, r + 1)); //存放新的回文 } } } } return ret; }
结合代码,以
babad
为例,分析动态规划实验④的结果建立在③上,⑦的结果建立在⑤上,对角线的关系;所以这次的循环是 r 不动,l 由上往下动
-
ManACher
算法 这个算法也叫马拉车算法,额…,是专门用于解决这个问题的,所以…,很强。
我是看了公众号
CSDN
的文章如何找到字符串中的最长回文子串才会的。它的核心主要也是中心扩展算法,为了避免讨论奇偶的情况,可以在字符串的每一个空隙中加上’#‘号,在LeetCode
的求解两个有序数组的中位数,也用到了这种预处理。 常规的中心扩展算法中,每一个中心点都要进行中心扩展,马拉车算法改进了这一点,并不是每个中心点都需要中心扩展的,假如字符串
dabcgcbaf
,我已经由d 到 g 都逐个进行了中心扩展,现得出第一个 c 的回文半径是0 (没有以 c 为中心的回文), 而 g 它的回文半径是3 (以 g 为中心,向左走3步,向右走3步,abc g cba
),那么还用对第二个 c 进行中心扩展吗?不用了,因为第一个和第二个 c 在以 g 为中心的回文范围内,利用对称性,既然第一个 c 的回文半径是0,那么第二个 c 也是0。以下图片来自
CSDN
的Manacher算法详解及模板(求解最长回文串)-
**已知
r1
和R
不需要对r
进行中心扩展的情况 1:**从左往右开始对每个点进行中心扩展,设r1
和R的回文半径已由中心扩展得到,到对 r 进行中心扩展的时候,发现右边界rightSide (R能到的最右端)
大于r,根据对称性得leftCenter = 2 * rightSideCenter - i;
i 就是 中心点 r ,rightSideCenter
就是中心点R,leftCenter
就是中心点r1
,得到leftCenter
之后,halfLenArr[leftCenter]
是中心点r1
的回文半径,r 的回文半径halfLenArr[i] = halfLenArr[leftCenter]
,下图中i + halfLenArr[i] < rightSide
即关于 r 的回文半径就已经由halfLenArr[leftCenter]
无需进行中心扩展。 -
**已知
r1
和R
不需要对r
进行中心扩展的情况 2:**跟 1 的情况差不多 -
**已知
r1
和R
需要对r
需要进行中心扩展:**需要对 r 的黄色部分进行探索
下面贴上大神写的代码:
// 预处理字符串,在两个字符之间加上# private String preHandleString(String s) { StringBuffer sb = new StringBuffer(); int len = s.length(); sb.append('#'); for(int i = 0; i < len; i++) { sb.append(s.charAt(i)); sb.append('#'); } return sb.toString(); } public String findLongestPlalindromeString(String s) { // 先预处理字符串 String str = preHandleString(s); // 处理后的字串长度 int len = str.length(); // 右边界 int rightSide = 0; // 右边界对应的回文串中心 int rightSideCenter = 0; // 保存以每个字符为中心的回文长度一半(向下取整) int[] halfLenArr = new int[len]; // 记录回文中心 int center = 0; // 记录最长回文长度 int longestHalf = 0; for(int i = 0; i < len; i++) { // 是否需要中心扩展 boolean needCalc = true; // 如果在右边界的覆盖之内 if(rightSide > i) { // 根据对称性,计算相对rightSideCenter的对称位置 int leftCenter = 2 * rightSideCenter - i; // 根据对称性,得到这两个的回文半径是一样的 halfLenArr[i] = halfLenArr[leftCenter]; //如果超过或等于右边界,即i + halfLenArr[i] >= rightside都要进行中心扩展 if(i + halfLenArr[i] >= rightSide) { halfLenArr[i] = rightSide - i; //先让它暂且等于右边界内的长度,对于这个长度之外部分(上图的黄色部分)再进行扩展 } // 如果根据已知条件计算得出的最长回文小于右边界,则不需要扩展了 if(i + halfLenArr[leftCenter] < rightSide) { // 直接推出结论 needCalc = false; } } // 中心扩展 if(needCalc) { while(i - 1 - halfLenArr[i] >= 0 && i + 1 + halfLenArr[i] < len) { if(str.charAt(i + 1 + halfLenArr[i]) == str.charAt(i - 1 - halfLenArr[i])) { halfLenArr[i]++; } else { break; } } // 更新右边界及中心 rightSide = i + halfLenArr[i]; rightSideCenter = i; // 记录最长回文串 if(halfLenArr[i] > longestHalf) { center = i; longestHalf = halfLenArr[i]; } } } // 去掉之前添加的# StringBuffer sb = new StringBuffer(); for(int i = center - longestHalf + 1; i <= center + longestHalf; i += 2) { sb.append(str.charAt(i)); } return sb.toString(); }
-