小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"是回文子序列,不满足“好字符串”的定义。根据题意,需要排除它们,只统计其余的非回文子序列数量。
本题的核心在于如何有效判断子序列是否为“好字符串”,并且对符合要求的子序列进行计数。由于所有可能的子序列数量呈指数级增长,暴力穷举方式将极其低效,不适合规模较大的输入,因此需要引入动态规划等高效的算法设计来完成这一任务。
解决思路
-
动态规划模型的构建:
本题使用动态规划进行解答。设dp[i][aa][bb]表示从字符串的第1个字符到第i个字符位置形成以字符bb结尾,且前一个字符为aa的子序列的数量。这个状态可以帮助我们快速确定子序列的组合,并检测其是否符合“好字符串”的定义。因为不满足“好字符串”的定义的字符串,必有形如“aa”或“aba”的子串,并且我们对空字符的值设定为0。 -
转移方程设计:
通过设计适当的状态转移方程,我们可以更新dp[i][aa][bb]的状态。假设c是当前字符,将dp[i][bb][c]更新为dp[i-1][aa][bb] + dp[i][bb][c]的模数,以记录以c为结尾的所有可能“好字符串”的数量。- 排除回文子序列:首先,如果
aa == bb或者c == aa,则以c结尾的序列将形成一个长度至少为2的回文串,应该被排除。因此,状态转移时,必须保证aa != bb且c != aa且c != bb。 - 继承之前状态:对于符合条件的状态,进行状态累加转移,将上一步的结果累加到当前的
dp[i][bb][c]中。
- 排除回文子序列:首先,如果
-
初始条件和边界处理:
dp[0][0][0] = 1:假设从空序列开始构建“好字符串”子序列。- 构造一个
sigma列表用于遍历ASCII码中‘a’到‘z’的字符和0(用于表示初始状态的空字符)。
-
最终结果计算:
通过遍历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)