1. 回文子串
(1)中心扩展
- 比如对一个字符串 ababa,选择最中间的 a 作为中心点,往两边扩散,第一次扩散发现 left 指向的是 b,right 指向的也是 b,所以是回文串,继续扩散,同理 ababa 也是回文串。
- 这个是确定了一个中心点后的寻找的路径,然后我们只要寻找到所有的中心点,问题就解决了。
- 中心点一共有多少个呢?看起来像是和字符串长度相等,但你会发现,如果是这样,上面的例子永远也搜不到 abab
- 中心点不能只有单个字符构成,还要包括两个字符,比如上面这个子串 abab,就可以有中心点 ba 扩展一次得到,所以最终的中心点由 2 * len - 1 个,分别是 len 个单字符和 len - 1 个双字符。
class Solution{
public int countSubstrings(String s) {
int ans = 0;
for (int center = 0; center < 2 * s.length() - 1; center++) {
// left和right指针和中心点的关系是?
// 首先是left,有一个很明显的2倍关系的存在,其次是right,可能和left指向同一个(偶数时),也可能往后移动一个(奇数)
// 大致的关系出来了,可以选择带两个特殊例子进去看看是否满足。
int left = center / 2;
int right = left + center % 2;
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
ans++;
left--;
right++;
}
}
return ans;
}
}
这个解法也同样适用于下面一个题:最长回文子串
(2)动规
- 状态:dp[i][j] 表示字符串s在[i,j]区间的子串是否是一个回文串。
- 状态转移方程:当 s[i] == s[j] && (j - i < 2 || dp[i + 1][j - 1]) 时,dp[i][j]=true,否则为false
解释一下:
- 当只有一个字符时,比如 a 自然是一个回文串。
- 当有两个字符时,如果是相等的,比如 aa,也是一个回文串。
- 当有三个及以上字符时,比如 ababa 这个字符记作串 1,把两边的 a 去掉,也就是 bab 记作串 2,可以看出只要串2是一个回文串,那么左右各多了一个 a 的串 1 必定也是回文串。所以当 s[i]==s[j] 时,自然要看 dp[i+1][j-1] 是不是一个回文串。
class Solution {
public int countSubstrings(String s) {
// 动态规划法
boolean[][] dp = new boolean[s.length()][s.length()];
int ans = 0;
for (int j = 0; j < s.length(); j++) {
for (int i = 0; i <= j; i++) {
//dp[i + 1][j - 1] : 向里面缩
if (s.charAt(i) == s.charAt(j) && (j - i < 2 || dp[i + 1][j - 1])) {
dp[i][j] = true;
ans++;
}
}
}
return ans;
}
}
2. 最长回文子串
给你一个字符串 s,找到 s 中最长的回文子串。
示例 1:
输入:s = “babad”
输出:“bab”
解释:“aba” 同样是符合题意的答案。
示例 2:
输入:s = “cbbd”
输出:“bb”
示例 3:
输入:s = “a”
输出:“a”
示例 4:
输入:s = “ac”
输出:“a”
提示:
1 <= s.length <= 1000
s 仅由数字和英文字母(大写和/或小写)组成
(1)暴力法
class Solution {
public String longestPalindrome(String s) {
String ans = "";
int max = 0;
int len = s.length();
for (int i = 0; i < len; i++)
for (int j = i + 1; j <= len; j++) {
String test = s.substring(i, j);
if (isPalindromic(test) && test.length() > max) {
ans = s.substring(i, j);
max = Math.max(max, ans.length());
}
}
return ans;
}
public boolean isPalindromic(String s) {
int len = s.length();
for (int i = 0; i < len / 2; i++) {
if (s.charAt(i) != s.charAt(len - i - 1)) {
return false;
}
}
return true;
}
}
(2)动态规划
dp[i][i]=1; //单个字符是回文串
dp[i][i+1]=1 if s[i]=s[i+1]; //连续两个相同字符是回文串
class Solution {
// 动态规划法
public String longestPalindrome(String s) {
int len = s.length();
if (len < 2) {
return s;
}
int maxLen = 1;
int begin = 0;
char[] cs = s.toCharArray();
// dp[i][j]:表示s[i][j]是否是回文串
boolean[][] dp = new boolean[len][len];
// 初始化:单独一个字符肯定是回文子串
for (int i = 0; i < len; i++) {
dp[i][i] = true;
}
// 经验:dp区域是正方形的话,通常左下角区域无效不需要再填,因为走过的区域不用再走
for (int j = 1; j < len; j++) { // 上三角区域,按列从上到下填
for (int i = 0; i < j; i++) {
// 首尾不相等时,必不是回文串
if (cs[i] != cs[j]) {
dp[i][j] = false;
} else {
// 首尾相等时,有2种情况
// 情况1:s[i...j]长度不超过3,不用检查必为回文串
// 情况2:s[i...j]长度大于3,由s[i+1...j-1]来判断
dp[i][j] = j - i + 1 <= 3 || dp[i + 1][j - 1];
}
// 更新max和begin
if (dp[i][j] && j - i + 1 > maxLen) {
maxLen = j - i + 1;
begin = i;
}
}
}
return s.substring(begin, begin + maxLen);
}
}
这样写或许更容易理解(更清晰):
class Solution {
public String longestPalindrome(String s) {
int len = s.length();
if (len < 2)
return s;
int maxStart = 0; //最长回文串的起点
int maxEnd = 0; //最长回文串的终点
int maxLen = 1; //最长回文串的长度
char[] cs = s.toCharArray();
// dp[i][j]:表示s[i][j]是否是回文串
boolean[][] dp = new boolean[len][len];
for(int right = 1; right < len; right++) {
for(int left = 0; left < right; left++) {
if (cs[left] == cs[right] && (right - left <= 2 || dp[left + 1][right - 1])) {
dp[left][right] = true;
// 更新max和begin
if (right - left + 1 > maxLen) {
maxLen = right - left + 1;
maxStart = left;
maxEnd = right;
}
}
}
}
return s.substring(maxStart, maxEnd + 1);
}
}
(3)中心扩展法
回文中心的两侧互为镜像。因此,回文可以从他的中心展开,并且只有 2n-1 个这样的中心(一个元素为中心的情况有 n 个,两个元素为中心的情况有 n-1 个)
class Solution {
public String longestPalindrome(String s) {
if (s == null || s.length() < 1){
return "";
}
// 初始化最大回文子串的起点,最大回文子串长度
int start = 0;
int maxLen = 1;
// 遍历每个位置,当做中心位
for (int i = 0; i < s.length(); i++) {
// 分别拿到奇数偶数的回文子串长度
int len_odd = expandCenter(s,i,i);
int len_even = expandCenter(s,i,i + 1);
// 对比最大的长度
int len = Math.max(len_odd,len_even);
// i为当前回文的中心:奇数回文为中心下标,偶数回文为一条虚线(i,i + 1)
// 计算对应最大回文子串的起点和终点,最长子串长度
//如果长度比原本记录的长度大了,说明有更长的回文出现了,所以要记录一下回文的下标
if (len > maxLen){
// 根据i和maxLen算start下标
// 奇数:i-maxLen/2
// 偶数:i-maxLen/2+1
// 统一:i-(maxLen-1)/2
//这里为什么要i-1?,这里说明一下,因为for循环是从0开始的,
//如果是奇数回文,假设有个回文是3个,那么len=3,此时中心i是下标1(从0开始),那么(len-1)/2和len/2的结果都是1,因为整型会向下取整
//但是如果是偶数回文,假设有个回文是4个,那么len=4,此时的中心是一条虚线,但是i的位置却在1,(因为S是从左向右遍历的,如果从右向左,
//i的位置就会在2.)这时候,(len-1)/2=1,len/2=2.很明显为了保证下标正确,我们需要的是(len-1)/2.原因其实是i在中心线的左边一位,
//所以要少减个1.
maxLen = len;
start = i - (len - 1)/2;
}
}
return s.substring(start, start + maxLen); //s.substring(3,5) 切割 [3,5),左闭右开
}
private int expandCenter(String s,int left,int right){
// left = right 的时候,此时回文中心是一个字符,回文串的长度是奇数
// right = left + 1 的时候,此时回文中心是一个空隙,回文串的长度是偶数
// 跳出循环的时候恰好满足 s.charAt(left) != s.charAt(right)
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)){
left--;
right++;
}
//这里其实是right-left+1-2,意思就是right-left+1是本来的长度,但是由于上面最后一次判断肯定false,所以最后一次left--和right++
//其实不属于回文的一部分,所以要减去2
return right - left - 1;
}
}
简化一下代码:
class Solution {
public String longestPalindrome(String s) {
if (s == null || s.length() < 1){
return "";
}
String result = "";
int len = s.length();
for (int center = 0; center < len * 2 - 1; center++) {
int left = center / 2;
int right = left + center % 2;
while (left >= 0 && right < len && s.charAt(left) == s.charAt(right)) {
String tmp = s.substring(left, right + 1);
if (tmp.length() > result.length())
result = tmp;
left--;
right++;
}
}
return result;
}
}
3. 最长回文子序列
(1)动规
状态:
- dp[i][j] 表示 s 的第 i 个字符到第 j 个字符组成的子串中,最长的回文序列长度是多少。
转移方程:
- 如果 s 的第 i 个字符和第 j 个字符相同的话: dp[i][j] = dp[i + 1][j - 1] + 2
- 如果 s 的第 i 个字符和第 j 个字符不同的话: dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
- 然后注意遍历顺序,i 从最后一个字符开始往前遍历,j 从 i + 1 开始往后遍历,这样可以保证每个子问题都已经算好了。
- 从递推公式dp[i][j] = dp[i + 1][j - 1] + 2 和 dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]) 可以看出,dp[i][j]是依赖于dp[i + 1][j - 1] 和 dp[i + 1][j],
- 如果我们想求dp[i][j],那么其他3个必须都是已知的,很明显从上往下遍历是不行的,我们只能让i从最后一个字符往前遍历,j从i的下一个开始遍历,最后只需要返回dp[0][length - 1]即可。
初始化:
- dp[i][i] = 1 单个字符的最长回文序列是 1
class Solution {
public int longestPalindromeSubseq(String s) {
int len = s.length();
int[][] dp = new int[len][len];
//这里i要从最后一个开始遍历
for(int i = len-1; i >= 0; --i) {
for(int j = i; j < len; ++j) {
//单个字符也是一个回文串
if(i == j) {
dp[i][j] = 1;
continue;
}
//j从i的下一个开始
if(s.charAt(i) == s.charAt(j)) {
dp[i][j] = dp[i+1][j-1] + 2;
} else {
dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]);
}
}
}
return dp[0][len-1];
}
}