小U的好字符串 | 豆包MarsCode AI 刷题

117 阅读5分钟

小U的好字符串

问题描述

小U定义了一个“好字符串”,它的要求是该字符串中不包含任意长度不小于2的回文子串。现在小U拿到了一个字符串,她想知道有多少个非空的子序列是“好字符串”。你的任务是帮助她计算出这些子序列的数量。

例如,对于字符串 "aba",它的子序列中除了 "aa" 和 "aba" 以外,其余五个子序列都是“好字符串”。

注意:由于答案可能非常大,你需要对结果取 109+7109+7 进行输出。


测试样例

样例1:

输入:s = "aba"
输出:5

样例2:

输入:s = "aaa"
输出:3

样例3:

输入:s = "ghij"
输出:15

问题分析

本题要求计算一个字符串中有多少个非空子序列是“小U定义的好字符串”。“好字符串”的定义是:不包含任意长度不小于2的回文子串。回文子串是一个正读和反读都相同的字符串,如“aa”或“aba”。换句话说,一个字符串被认为是“好字符串”,当它的任意长度不小于2的连续子串都不是回文串。 以字符串 "aba" 为例,其非空子序列包括:

  • 单字符子序列:"a", "b", "a"
  • 双字符子序列:"ab", "ba", "aa"
  • 三字符子序列:"aba"

其中,"aa"和"aba"是回文子序列,不满足“好字符串”的定义。根据题意,需要排除它们,只统计其余的非回文子序列数量。

本题的核心在于如何有效判断子序列是否为“好字符串”,并且对符合要求的子序列进行计数。由于所有可能的子序列数量呈指数级增长,暴力穷举方式将极其低效,不适合规模较大的输入,因此需要引入动态规划等高效的算法设计来完成这一任务。

解决思路

  1. 动态规划模型的构建
    本题使用动态规划进行解答。设 dp[i][aa][bb] 表示从字符串的第1个字符到第 i 个字符位置形成以字符 bb 结尾,且前一个字符为 aa 的子序列的数量。这个状态可以帮助我们快速确定子序列的组合,并检测其是否符合“好字符串”的定义。因为不满足“好字符串”的定义的字符串,必有形如“aa”或“aba”的子串,并且我们对空字符的值设定为0。

  2. 转移方程设计
    通过设计适当的状态转移方程,我们可以更新 dp[i][aa][bb] 的状态。假设 c 是当前字符,将 dp[i][bb][c] 更新为 dp[i-1][aa][bb] + dp[i][bb][c] 的模数,以记录以 c 为结尾的所有可能“好字符串”的数量。

    1. 排除回文子序列:首先,如果 aa == bb 或者 c == aa,则以 c 结尾的序列将形成一个长度至少为2的回文串,应该被排除。因此,状态转移时,必须保证 aa != bbc != aac != bb
    2. 继承之前状态:对于符合条件的状态,进行状态累加转移,将上一步的结果累加到当前的 dp[i][bb][c] 中。
  3. 初始条件和边界处理

    • dp[0][0][0] = 1:假设从空序列开始构建“好字符串”子序列。
    • 构造一个 sigma 列表用于遍历ASCII码中‘a’到‘z’的字符和0(用于表示初始状态的空字符)。
  4. 最终结果计算
    通过遍历 dp[sz][aa][bb] 中符合条件的所有状态,将其累加得到最终的符合条件的“好字符串”子序列数量,在这要注意不统计 dp[sz][0][0]这个空字符串的答案。

代码中状态转移的部分:

    for i in range(1, sz + 1):
        c = ord(s[i - 1])  
        for aa in sigma:
            for bb in sigma:
                if (aa == 0 or aa != bb) and (c != aa) and (c != bb):
                    dp[i][bb][c] = (dp[i - 1][aa][bb] + dp[i][bb][c]) % mod
                dp[i][aa][bb] = (dp[i - 1][aa][bb] + dp[i][aa][bb]) % mod

总结

本题的核心在于通过动态规划高效地统计符合“好字符串”定义的子序列数量。所谓“好字符串”,即不包含任意长度大于等于 2 的回文子串。直接枚举所有可能的子序列会导致指数级增长的复杂度,因此我们采用了动态规划的方式,通过状态转移实现了更高效的解法。

完整的代码如下:

def solution(s: str) -> int:
    mod = int(1e9 + 7)
    sz = len(s)

    # dp数组:初始化为三维数组,每个元素初始为0
    dp = [[[0] * 300 for _ in range(300)] for _ in range(sz + 1)]
    dp[0][0][0] = 1

    # sigma数组,用于存储a-z字符的ASCII码和0
    sigma = [i for i in range(ord('a'), ord('z') + 1)]
    sigma.append(0)

    # 动态规划计算
    for i in range(1, sz + 1):
        c = ord(s[i - 1])  # 当前字符的ASCII码
        for aa in sigma:
            for bb in sigma:
                # 判断是否符合条件
                if (aa == 0 or aa != bb) and (c != aa) and (c != bb):
                    dp[i][bb][c] = (dp[i - 1][aa][bb] + dp[i][bb][c]) % mod
                dp[i][aa][bb] = (dp[i - 1][aa][bb] + dp[i][aa][bb]) % mod

    # 计算最终答案
    ans = 0
    for aa in sigma:
        for bb in sigma:
            ans = (ans + dp[sz][aa][bb]) % mod
    ans = (ans + mod - dp[sz][0][0]) % mod
    return ans

# 测试
print(solution("aba") == 5)
print(solution("aaa") == 3)
print(solution("ghij") == 15)