题目描述

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。

回文字符串 是正着读和倒过来读一样的字符串。

子字符串 是字符串中的由连续字符组成的一个序列。

示例 1:

1
2
3
输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"

具体见题目详解

算法思路

很明显的读到题目就能想到动态规划,因为传统暴力:对于每个子串组合,分别判断其是否回文,时间复杂度最少是 $O(n^3)$,百分之百过不了测试,因为其做了很多重复工作,比如s[i~j]如果已经知道是否是回文之后,对于s[i-1, j+1](左右各拓展一位),我们只需要判断s[i-1]==s[j+1]即可,因此可以采用动态规划的方法:

  1. 对于i==j,那么dp[i][j]=1是必然的,因为只有一个字符
  2. 对于i==j-1,也就是双字符,如果两个字符相等,那么dp[i][j]=1,否则就是0
  3. 其余情况,我们只需要判断最左边和最右边字符是否相等并且中间的子串是回文串即可

因此得到状态转移方程如下:

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Solution {
public:
int countSubstrings(string s) {
// 单个字符必为回文子串,两个字符需要相等才为回文子串
// dp[i][j]表示s[i~j]是否是回文子串
// 状态转移方程:dp[i][j] = dp[i+1][j-1] && s[i] == s[j]
int ans = 0;
int n = s.length();
ans += n;
vector < vector <int> > dp(n, vector<int>(n, 0));
// i 从 n - 1 ~ 0
// j 从 i ~ n - 1
for (int i = n - 1; i >= 0; i--) {
for (int j = i; j < n; j++) {
if (i == j) {
dp[i][j] = 1;
}
else if (i == j - 1) {
if (s[i] == s[j]) {
dp[i][j] = 1;
ans += 1;
}
}
else {
if (dp[i + 1][j - 1] && s[i] == s[j]) {
dp[i][j] = 1;
ans += 1;
}
}
}
}
return ans;
}
};

时间复杂度和空间复杂度均为$O(n^2)$,我也只能想到这了。

思路借鉴

我能看懂的一个官方题解是使用双指针,具体而言可以这样解释:

  1. 一个回文子串必定有回文中心,也就是子串的中心
  2. 对于单回文中心,也就是子串长度为奇数,那么可以从中心向两边拓展,如果两边的字符是一样的,就可以进行拓展,否则就终止
  3. 对于双回文中心,也就是子串长度为偶数,那么也可以按照上述方式进行相同的拓展

对于单回文中心和双回文中心,可以使用两个循环,也可以只使用一个,具体他怎么想出来的可以直接看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
int countSubstrings(string s) {
int n = s.size(), ans = 0;
for (int i = 0; i < 2 * n - 1; ++i) {
int l = i / 2, r = i / 2 + i % 2;
while (l >= 0 && r < n && s[l] == s[r]) {
--l;
++r;
++ans;
}
}
return ans;
}
};