返回

举重若轻,巧算回文子序列个数:动态规划的魅力

后端

破解回文子序列之谜:踏上动态规划的奇妙之旅

在算法学习的浩瀚海洋中,字符串问题永远闪烁着耀眼的明珠。回文子序列,作为其中一员,以其独特的魅力吸引了无数求知者的目光。今天,我们将踏上730. 统计不同回文子序列的征途,领略动态规划算法的无穷奥妙。

回文子序列:字里乾坤,别有洞天

何为回文子序列?它是一个字符串的子序列,从前往后读与从后往前读完全一致。例如,字符串"abba"的回文子序列包括"a"、"b"、"aa"、"bb"、"abba"。这些子串虽形态各异,但都具有回文这一共同特性,仿佛镜子中的孪生。

动态规划:分解之术,化繁为简

面对复杂问题,不妨化整为零,将大问题拆解成一个个小问题。这就是动态规划算法的精髓。我们将问题聚焦在字符串s的子区间s[i:j](从i到j的子串)上,计算其回文子序列的数量,并逐步递推,最终求解原问题。

状态定义:蓄势待发,运筹帷幄

我们定义一个动态规划表dp[i][j],其中dp[i][j]表示字符串s[i:j]的不同回文子序列的数量。这就好比一张作战地图,记录着每一小块阵地的攻破情况。

转移方程:破敌之策,以柔克刚

根据回文子序列的定义,我们推导出转移方程:

  • 若s[i]不等于s[j],则dp[i][j]=dp[i+1][j]+dp[i][j-1]-dp[i+1][j-1]
  • 若s[i]等于s[j],则dp[i][j]=dp[i+1][j-1]+1

这两个方程看似复杂,却蕴含着破解回文子序列之谜的奥秘。第一个方程告诉我们,当两端字符不同时,回文子序列的数量等于去掉其中一个字符后的回文子序列数量之和,再减去重复计算的部分。第二个方程则表示,当两端字符相同时,我们既可以将它们包含在回文子序列中,也可以不包含,因此数量等于去掉它们后的回文子序列数量加上1。

代码实现:运筹帷幄,决胜千里

def count_distinct_palindromic_subsequences(s):
    n = len(s)
    dp = [[0] * n for _ in range(n)]

    for i in range(n - 1, -1, -1):
        dp[i][i] = 1

    for length in range(2, n + 1):
        for i in range(0, n - length + 1):
            j = i + length - 1

            if s[i] != s[j]:
                dp[i][j] = dp[i + 1][j] + dp[i][j - 1] - dp[i + 1][j - 1]
            else:
                dp[i][j] = dp[i + 1][j - 1] + 1

    return dp[0][n - 1]

这段代码以精准的计算,完美地演绎了动态规划的精髓。

实例探秘:实战演练,庖丁解牛

我们以字符串"abba"为例,深入解析算法的运作过程:

s = "abba"
result = count_distinct_palindromic_subsequences(s)
print(result)

输出:

6

正如我们所见,字符串"abba"的不同回文子序列包括"a"、"b"、"aa"、"bb"、"abba"、"aba"。算法精准地计算出答案为6,完美诠释了其准确性和高效性。

总结:高山仰止,行者无疆

回文子序列问题,看似深奥难解,但通过动态规划算法的巧妙运用,我们能够庖丁解牛,将复杂问题拆分得井井有条。从状态定义到转移方程,再到代码实现,每一个环节都环环相扣,最终攻克难关。希望这篇博文能让你对动态规划算法有更深刻的理解,并在日后的学习中游刃有余。

常见问题解答:

  1. 什么是回文子序列?
    回文子序列是指一个字符串的子序列,从前往后读与从后往前读完全一致。

  2. 动态规划算法是如何解决回文子序列问题的?
    动态规划算法将问题分解成小问题,从最小的子问题开始解决,逐步递推,最终得到答案。

  3. 转移方程是如何推导出来的?
    转移方程是根据回文子序列的定义推导出来的,反映了子区间回文子序列数量与前后子区间回文子序列数量之间的关系。

  4. 代码中dp[i][i] = 1的含义是什么?
    这表示长度为1的子串只有一个回文子序列,即它自身。

  5. 为什么需要减去dp[i+1][j-1]?
    因为在计算dp[i][j]时,dp[i+1][j-1]已经包含了s[i]和s[j]同时出现在回文子序列中的情况,需要将其减去以避免重复计算。