leetcode-master/problems/0647.回文子串.md

322 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<p align="center">
<a href="https://mp.weixin.qq.com/s/RsdcQ9umo09R6cfnwXZlrQ"><img src="https://img.shields.io/badge/PDF下载-代码随想录-blueviolet" alt=""></a>
<a href="https://mp.weixin.qq.com/s/b66DFkOp8OOxdZC_xLZxfw"><img src="https://img.shields.io/badge/刷题-微信群-green" alt=""></a>
<a href="https://space.bilibili.com/525438321"><img src="https://img.shields.io/badge/B站-代码随想录-orange" alt=""></a>
<a href="https://mp.weixin.qq.com/s/QVF6upVMSbgvZy8lHZS3CQ"><img src="https://img.shields.io/badge/知识星球-代码随想录-blue" alt=""></a>
</p>
<p align="center"><strong>欢迎大家<a href="https://mp.weixin.qq.com/s/tqCxrMEU-ajQumL1i8im9A">参与本项目</a>,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!</strong></p>
## 647. 回文子串
题目链接https://leetcode-cn.com/problems/palindromic-substrings/
给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
示例 1
输入:"abc"
输出3
解释:三个回文子串: "a", "b", "c"
示例 2
输入:"aaa"
输出6
解释6个回文子串: "a", "a", "a", "aa", "aa", "aaa"
提示:
输入的字符串长度不会超过 1000 。
## 暴力解法
两层for循环遍历区间起始位置和终止位置然后判断这个区间是不是回文。
时间复杂度O(n^3)
## 动态规划
动规五部曲:
1. 确定dp数组dp table以及下标的含义
布尔类型的dp[i][j]:表示区间范围[i,j] 注意是左闭右闭的子串是否是回文子串如果是dp[i][j]为true否则为false。
2. 确定递推公式
在确定递推公式时,就要分析如下几种情况。
整体上是两种就是s[i]与s[j]相等s[i]与s[j]不相等这两种。
当s[i]与s[j]不相等那没啥好说的了dp[i][j]一定是false。
当s[i]与s[j]相等时,这就复杂一些了,有如下三种情况
* 情况一下标i 与 j相同同一个字符例如a当然是回文子串
* 情况二下标i 与 j相差为1例如aa也是文子串
* 情况三下标i 与 j相差大于1的时候例如cabac此时s[i]与s[j]已经相同了我们看i到j区间是不是回文子串就看aba是不是回文就可以了那么aba的区间就是 i+1 与 j-1区间这个区间是不是回文就看dp[i + 1][j - 1]是否为true。
以上三种情况分析完了,那么递归公式如下:
```C++
if (s[i] == s[j]) {
if (j - i <= 1) { // 情况一 和 情况二
result++;
dp[i][j] = true;
} else if (dp[i + 1][j - 1]) { // 情况三
result++;
dp[i][j] = true;
}
}
```
result就是统计回文子串的数量。
注意这里我没有列出当s[i]与s[j]不相等的时候因为在下面dp[i][j]初始化的时候就初始为false。
3. dp数组如何初始化
dp[i][j]可以初始化为true么 当然不行,怎能刚开始就全都匹配上了。
所以dp[i][j]初始化为false。
4. 确定遍历顺序
遍历顺序可有有点讲究了。
首先从递推公式中可以看出情况三是根据dp[i + 1][j - 1]是否为true在对dp[i][j]进行赋值true的。
dp[i + 1][j - 1] 在 dp[i][j]的左下角,如图:
![647.回文子串](https://img-blog.csdnimg.cn/20210121171032473.jpg)
如果这矩阵是从上到下从左到右遍历那么会用到没有计算过的dp[i + 1][j - 1],也就是根据不确定是不是回文的区间[i+1,j-1],来判断了[i,j]是不是回文,那结果一定是不对的。
**所以一定要从下到上从左到右遍历这样保证dp[i + 1][j - 1]都是经过计算的**。
有的代码实现是优先遍历列然后遍历行其实也是一个道理都是为了保证dp[i + 1][j - 1]都是经过计算的。
代码如下:
```C++
for (int i = s.size() - 1; i >= 0; i--) { // 注意遍历顺序
for (int j = i; j < s.size(); j++) {
if (s[i] == s[j]) {
if (j - i <= 1) { // 情况一 和 情况二
result++;
dp[i][j] = true;
} else if (dp[i + 1][j - 1]) { // 情况三
result++;
dp[i][j] = true;
}
}
}
}
```
5. 举例推导dp数组
举例,输入:"aaa"dp[i][j]状态如下:
![647.回文子串1](https://img-blog.csdnimg.cn/20210121171059951.jpg)
图中有6个true所以就是有6个回文子串。
**注意因为dp[i][j]的定义所以j一定是大于等于i的那么在填充dp[i][j]的时候一定是只填充右上半部分**
以上分析完毕C++代码如下:
```C++
class Solution {
public:
int countSubstrings(string s) {
vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
int result = 0;
for (int i = s.size() - 1; i >= 0; i--) { // 注意遍历顺序
for (int j = i; j < s.size(); j++) {
if (s[i] == s[j]) {
if (j - i <= 1) { // 情况一 情况二
result++;
dp[i][j] = true;
} else if (dp[i + 1][j - 1]) { // 情况三
result++;
dp[i][j] = true;
}
}
}
}
return result;
}
};
```
以上代码是为了凸显情况一二三,当然是可以简洁一下的,如下:
```C++
class Solution {
public:
int countSubstrings(string s) {
vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
int result = 0;
for (int i = s.size() - 1; i >= 0; i--) {
for (int j = i; j < s.size(); j++) {
if (s[i] == s[j] && (j - i <= 1 || dp[i + 1][j - 1])) {
result++;
dp[i][j] = true;
}
}
}
return result;
}
};
```
* 时间复杂度O(n^2)
* 空间复杂度O(n^2)
## 双指针法
动态规划的空间复杂度是偏高的我们再看一下双指针法
首先确定回文串就是找中心然后想两边扩散看是不是对称的就可以了
**在遍历中心点的时候,要注意中心点有两种情况**
一个元素可以作为中心点两个元素也可以作为中心点
那么有人同学问了三个元素还可以做中心点呢其实三个元素就可以由一个元素左右添加元素得到四个元素则可以由两个元素左右添加元素得到
所以我们在计算的时候要注意一个元素为中心点和两个元素为中心点的情况
**这两种情况可以放在一起计算,但分别计算思路更清晰,我倾向于分别计算**代码如下
```C++
class Solution {
public:
int countSubstrings(string s) {
int result = 0;
for (int i = 0; i < s.size(); i++) {
result += extend(s, i, i, s.size()); // 以i为中心
result += extend(s, i, i + 1, s.size()); // 以i和i+1为中心
}
return result;
}
int extend(const string& s, int i, int j, int n) {
int res = 0;
while (i >= 0 && j < n && s[i] == s[j]) {
i--;
j++;
res++;
}
return res;
}
};
```
* 时间复杂度O(n^2)
* 空间复杂度O(1)
## 其他语言版本
Java
动态规划:
```java
class Solution {
public int countSubstrings(String s) {
int len, ans = 0;
if (s == null || (len = s.length()) < 1) return 0;
//dp[i][j]s字符串下标i到下标j的字串是否是一个回文串即s[i, j]
boolean[][] dp = new boolean[len][len];
for (int j = 0; j < len; j++) {
for (int i = 0; i <= j; i++) {
//当两端字母一样时,才可以两端收缩进一步判断
if (s.charAt(i) == s.charAt(j)) {
//i++j--即两端收缩之后i,j指针指向同一个字符或者i超过j了,必然是一个回文串
if (j - i < 3) {
dp[i][j] = true;
} else {
//否则通过收缩之后的字串判断
dp[i][j] = dp[i + 1][j - 1];
}
} else {//两端字符不一样,不是回文串
dp[i][j] = false;
}
}
}
//遍历每一个字串,统计回文串个数
for (int i = 0; i < len; i++) {
for (int j = 0; j < len; j++) {
if (dp[i][j]) ans++;
}
}
return ans;
}
}
```
中心扩散法
```java
class Solution {
public int countSubstrings(String s) {
int len, ans = 0;
if (s == null || (len = s.length()) < 1) return 0;
//总共有2 * len - 1个中心点
for (int i = 0; i < 2 * len - 1; i++) {
//通过遍历每个回文中心,向两边扩散,并判断是否回文字串
//有两种情况left == rightright = left + 1这两种回文中心是不一样的
int left = i / 2, right = left + i % 2;
while (left >= 0 && right < len && s.charAt(left) == s.charAt(right)) {
//如果当前是一个回文串,则记录数量
ans++;
left--;
right++;
}
}
return ans;
}
}
```
Python
Go
```Go
func countSubstrings(s string) int {
res:=0
dp:=make([][]bool,len(s))
for i:=0;i<len(s);i++{
dp[i]=make([]bool,len(s))
}
for i:=len(s)-1;i>=0;i--{
for j:=i;j<len(s);j++{
if s[i]==s[j]{
if j-i<=1{
res++
dp[i][j]=true
}else if dp[i+1][j-1]{
res++
dp[i][j]=true
}
}
}
}
return res
}
```
-----------------------
* 作者微信[程序员Carl](https://mp.weixin.qq.com/s/b66DFkOp8OOxdZC_xLZxfw)
* B站视频[代码随想录](https://space.bilibili.com/525438321)
* 知识星球[代码随想录](https://mp.weixin.qq.com/s/QVF6upVMSbgvZy8lHZS3CQ)
<div align="center"><img src=../pics/公众号.png width=450 alt=> </img></div>