【字符串】力扣647:回文子串

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

示例:

输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"

暴力破解法

两层for循环,遍历区间起始位置和终止位置,然后判断这个区间是不是回文。
时间复杂度:O(n^3)

中心拓展

计算有多少个回文子串的最朴素方法就是枚举出所有的回文子串,而枚举出所有的回文字串又有两种思路,分别是:

  • 枚举出所有的子串,然后再判断这些子串是否是回文;
  • 双指针:枚举每一个可能的回文中心,然后用两个指针分别向左右两边拓展,当两个指针指向的元素相同的时候就拓展,否则停止拓展。

假设字符串的长度为 n,前者会用 O(n^2)的时间枚举出所有的子串 s[\(l_i\)\(r_i\)],然后再用 O(\(r_i\) - \(l_i\) + 1) 的时间检测当前的子串是否是回文,整个算法的时间复杂度是 O(n^3);而后者枚举回文中心的次数是 O(n) 的,对于每个回文中心拓展的次数也是 O(n) 的,所以时间复杂度是 O(n^2)。所以最好是选择第二种方法来枚举所有的回文子串。

在实现的时候要处理一个问题:如何有序地枚举所有可能的回文中心?

考虑回文长度是奇数和回文长度是偶数的两种情况:

  • 如果回文长度是奇数,那么回文中心是一个字符
  • 如果回文长度是偶数,那么中心是两个字符

中心点总数看起来像是和字符串长度相等,但如果是这样,上面的例子永远也搜不到 abab,想象一下单个字符的哪个中心点扩展可以得到这个子串?似乎不可能。所以中心点不能只有单个字符构成,还要包括两个字符,比如上面这个子串 abab,就可以有中心点 ba 扩展一次得到,所以最终的中心点有 2 * len - 1 个,分别是 len 个单字符和 len - 1 个双字符。

1. 两种中心点情况分别计算

class Solution:
    def countSubstrings(self, s: str) -> int:
        n = len(s)
        count = 0 # 初始化回文子串数目
        if n == 0 or n == 1:
            return n
        for i in range(n):
            count += self.extend(s, i, i, n) # 以 i 为中心
            count += self.extend(s, i, i + 1, n) # 以 i 和 i+1 为中心
        return count

    def extend(self, s, i, j, n):
        res = 0
        while i >= 0 and j < n and s[i] == s[j]:
            res += 1
            i -= 1
            j += 1
        return res

时间复杂度:O(n^2)。
空间复杂度:O(1)。

2. 两种中心点情况一起计算

这里要想清楚的是两个中心点left和right的关系:right = left + center % 2

class Solution:
    def countSubstrings(self, s: str) -> int:
        n = len(s)
        count = 0 # 初始化回文子串数目
        if n == 0 or n == 1:
            return n
        for center in range(2 * n + 1):
            left = center // 2 # 注意left必须是整数,符号为//而非/
            right = left + center % 2 # 当 left 为偶数,只有1个中心点;当 left 为奇数,有2个中心点
            while left >= 0 and right < n and s[left] == s[right]:
                count += 1
                left -= 1
                right += 1
        return count

动态规划

  1. 确定 dp 数组以及下标的含义

dp[i][j]为布尔类型:表示区间范围 [i,j] (注意是左闭右闭)的子串是否是回文子串,如果是,那么dp[i][j]为true,否则为false。

  1. 确定递推公式

在确定递推公式时,就要分情况分析。整体上是两种:

  • 当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。

以上三种情况分析完了,那么 dp 为 True 的递归公式为:

\[dp[i][j] = True \quad j - i <= 1 \ or \ dp[i + 1] [j - 1] = True \]

  1. dp数组初始化

dp[i][j]可以初始化为True么? 当然不行,怎么能刚开始就全都匹配上了。所以dp[i][j]初始化为False。

  1. 确定遍历顺序

首先从递推公式中可以看出,情况三是根据dp[i + 1][j - 1]是否为True,在对dp[i][j]进行赋值True的。在矩阵中,dp[i][j]在dp[i + 1][j - 1]的右上角,要保证计算dp[i][j]时dp[i + 1][j - 1]已经计算出来,就得先遍历列再遍历行,即从下到上,从左到右遍历。

class Solution:
    def countSubstrings(self, s: str) -> int:
        dp = [[False] * len(s) for _ in range(len(s))] # 二维数组
        result = 0
        for i in range(len(s) - 1, -1, -1): # 注意遍历顺序,注意 i 从 len(s) - 1 开始
            for j in range(i, len(s)):
                if s[i] == s[j]:
                    if j - i <= 1: # 情况一 和 情况二
                        result += 1
                        dp[i][j] = True
                    elif dp[i+1][j-1]: # 情况三
                        result += 1
                        dp[i][j] = True
        return result

作者:carlsun-2
链接:https://leetcode.cn/problems/palindromic-substrings/solution/dai-ma-sui-xiang-lu-dai-ni-xue-tou-dpzi-vidge/

简洁版

class Solution:
    def countSubstrings(self, s: str) -> int:
        dp = [[False] * len(s) for _ in range(len(s))]
        result = 0
        for i in range(len(s)-1, -1, -1):
            for j in range(i, len(s)):
                if s[i] == s[j] and (j - i <= 1 or dp[i+1][j-1]): 
                    result += 1
                    dp[i][j] = True
        return result

作者:carlsun-2
链接:https://leetcode.cn/problems/palindromic-substrings/solution/dai-ma-sui-xiang-lu-dai-ni-xue-tou-dpzi-vidge/

时间复杂度:O(n^2)
空间复杂度:O(n^2)

posted @ 2022-07-01 16:56  Vonos  阅读(108)  评论(0)    收藏  举报