剑指 Offer II 020. 回文子字符串的个数

Posted 炫云云

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了剑指 Offer II 020. 回文子字符串的个数相关的知识,希望对你有一定的参考价值。

剑指 Offer II 020. 回文子字符串的个数

给定一个字符串 s ,请计算这个字符串中有多少个回文子字符串。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

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

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

动态规划

对于一个子串而言,如果它是回文串,并且长度大于 2 , 那么将它首尾的两个字母去除之后,它仍 然是个回文串。例如对于字符串 “ababa”, 如果我们已经知道 “bab" 是回文串,那么 "ababa"一定 是回文串, 这是因为它的首尾两个字母都是 “a”。

根据这样的思路,我们就可以用动态规划的方法解决本题。

第 1 步:定义状态

我们用 d P ( i , j ) dP(i, j) dP(i,j)​ 表示字符串 s s s​ 的第 i i i​ 到 j j j​ 个字母组成的串 (下文表示成 s [ i : j ] ) s[i: j]) s[i:j])​​ 是否为回文串:
d P ( i , j ) = {  true,   如果子串  S i … S j  是回文串   false,   其它情况  dP(i, j)= \\begin{cases}\\text { true, } & \\text { 如果子串 } S_{i} \\ldots S_{j} \\text { 是回文串 } \\\\ \\text { false, } & \\text { 其它情况 }\\end{cases} dP(i,j)={ true,  false,  如果子串 SiSj 是回文串  其它情况 
这里的「其它情况」包含两种可能性:

  • s [ i , j ] s[i, j] s[i,j] 本身不是一个回文串;
  • i > j i>j i>j, 此时 s [ i , j ] s[i, j] s[i,j]​ 本身不合法。

第 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

那么我们就可以写出动态规划的状态转移方程:
d P ( i , j ) = d P ( i + 1 , j − 1 )   a n d   ( S i = = S j ) dP(i, j)=dP(i+1, j-1)~and ~\\left(S_{i}==S_{j}\\right) dP(i,j)=dP(i+1,j1) and (Si==Sj)
也就是说,只有 s [ i + 1 : j − 1 ] s[i+1: j-1] s[i+1:j1] 是回文串,并且 s s s 的第 i i i j j j 个字母相同时, s [ i : j ] s[i: j] s[i:j]​ 才会是回文串。

第 3步:确定遍历顺序

首先从递推公式中可以看出,情况三是根据dp[i + 1][j - 1]是否为true,在对dp[i][j]进行赋值true的。

dp[i + 1][j - 1]dp[i][j]的左下角,如图:

  • 「动态规划」的「自底向上」求解问题的思路,很多时候是在填写一张二维表格。由于 s[i..j] 表示 s 的一个子串,因此 ij 的关系是 i <= j,只需要填这张表格对角线右上半部分

    所以一定要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]都是经过计算的

举例,输入:“aaa”,dp[i][j]状态如下:

图中有6个true,所以就是有6个回文子串。

  • 时间复杂度: O ( n 2 ) O(n^2) O(n2)
  • 空间复杂度: O ( n 2 ) O(n^2) O(n2)
class Solution:
    def countSubstrings(self, s: str) -> int:
        n = len(s)
        dp = [[False]* n for _ in range(n) ]
        result = 0
        for i in range(n-1,-1,-1):# 自底向上
            for j in range(i,n):
                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

双指针

动态规划的空间复杂度是偏高的,我们再看一下双指针法。

首先确定回文串,就是找中心然后想两边扩散看是不是对称的就可以了。

在遍历中心点的时候,要注意中心点有两种情况。

一个元素可以作为中心点,两个元素也可以作为中心点。

那么有人同学问了,三个元素还可以做中心点呢。其实三个元素就可以由一个元素左右添加元素得到,四个元素则可以由两个元素左右添加元素得到。

所以我们在计算的时候,要注意一个元素为中心点和两个元素为中心点的情况。

这两种情况可以放在一起计算,但分别计算思路更清晰,我倾向于分别计算,代码如下:

class Solution:
    def countSubstrings(self, s: str) -> int:
        result = 0
        n = len(s)
        for i in range(n):
            #通过遍历每个回文中心,向两边扩散,并判断是否回文字串
            result += self.extend(s, i, i, n) #以i为中心
            result += self.extend(s, i, i+1, n) #以i和i+1为中心
        return result

    def extend(self, s, i, j, n):
        res = 0
        while i >= 0 and j < n and s[i] == s[j]:
            #如果当前是一个回文串,则记录数量
            i -= 1 #向两边扩散
            j += 1
            res += 1
        return res


以上是关于剑指 Offer II 020. 回文子字符串的个数的主要内容,如果未能解决你的问题,请参考以下文章

剑指 Offer II 020. 回文子字符串的个数

剑指Offer018 - 020 刷题笔记 回文 双指针DP中心扩展

剑指 Offer II 086. 分割回文子字符串

剑指 Offer II 086. 分割回文子字符串

Java每日一题——>剑指 Offer II 027. 回文链表

LeetCode Algorithm 剑指 Offer II 027. 回文链表