问题描述
小U定义了一个"好字符串",它的要求是该字符串中不包含任意长度不小于2的回文子串。现在小U拿到了一个字符串,她想知道有多少个非空的子序列是"好字符串"。你的任务是帮助她计算出这些子序列的数量。
例如,对于字符串 "aba",它的子序列中除了 "aa" 和 "aba" 以外,其余五个子序列都是"好字符串"。
注意:由于答案可能非常大,你需要对结果取输出。
问题解决
对于这个问题,我有了如下思路:
- 首先需要理解什么是'好字符串':不包含长度≥2的回文子串
- 然后需要考虑如何生成所有子序列
- 对于每个子序列,需要判断它是否是'好字符串'
- 最后需要处理大数取模的问题
由此得出总体思路:先写一个函数判断字符串是否为好字符串,然后再考虑如何高效地生成和统计子序列。
基于此,我写出了第一版代码,是一个暴力解法:
def is_good_string(s: str) -> bool:
n = len(s)
for i in range(n):
for j in range(i + 1, n):
substr = s[i:j + 1]
if len(substr) >= 2 and substr == substr[::-1]:
return False
return True
MOD = 10**9 + 7
n = len(s)
count = 0
for mask in range(1, 1 << n):
subsequence = ''
for i in range(n):
if mask & (1 << i):
subsequence += s[i]
if is_good_string(subsequence):
count = (count + 1) % MOD
return count
在进行测试运行和提交后,发现都成功通过了。
但当我让豆包对我的代码进行检查后,豆包给出了如下评价:
你的代码尝试通过生成所有可能的子序列并检查它们是否为“好字符串”来解决问题。虽然这种方法在逻辑上是正确的,但它的时间复杂度非常高,可能会导致性能问题,尤其是在输入字符串较长的情况下。
### 代码检查
1. **子序列生成**:
- 你使用位掩码(bitmask)来生成所有可能的子序列。这种方法在字符串长度较小时是可行的,但对于较长的字符串,生成的子序列数量会呈指数增长。
1. **回文检查**:
- 你使用嵌套循环来检查每个子序列是否包含长度不小于2的回文子串。这种方法的时间复杂度是 (O(n^2)) 对于每个子序列,其中 (n) 是子序列的长度。
1. **性能问题**:
- 对于长度为 (n) 的字符串,子序列的总数是 (2^n - 1)。对于每个子序列,你还需要进行 (O(n^2)) 的回文检查。因此,总的时间复杂度是 (O(2^n \cdot n^2)),这在 (n) 较大时会非常慢。
### 优化建议
1. **动态规划**:
- 考虑使用动态规划来优化回文检查。你可以维护一个二维数组 `dp[i][j]` 来表示子串 `s[i:j+1]` 是否是回文。这样可以避免重复计算。
1. **子序列生成优化**:
- 可以考虑使用递归或迭代的方式来生成子序列,并结合动态规划来减少不必要的计算。
1. **剪枝**:
- 在生成子序列的过程中,如果发现某个子序列已经包含回文,可以提前终止该子序列的生成。
并且给出了相应的优化代码:
def solution(s: str) -> int:
MOD = 10**9 + 7
n = len(s)
# 动态规划数组,dp[i][j] 表示 s[i:j+1] 是否是回文
dp = [[False] * n for _ in range(n)]
# 初始化长度为1的子串为回文
for i in range(n):
dp[i][i] = True
# 计算所有子串是否为回文
for length in range(2, n + 1):
for i in range(n - length + 1):
j = i + length - 1
if s[i] == s[j]:
if length == 2 or dp[i + 1][j - 1]:
dp[i][j] = True
# 计算好字符串的数量
count = 0
for mask in range(1, 1 << n):
subsequence = ''
for i in range(n):
if mask & (1 << i):
subsequence += s[i]
if is_good_string(subsequence, dp):
count = (count + 1) % MOD
return count
def is_good_string(s: str, dp) -> bool:
n = len(s)
for i in range(n):
for j in range(i + 1, n):
if dp[i][j]:
return False
return True
根据这个思路,我优化了一遍原代码:
def solution(s: str) -> int:
MOD = 10**9 + 7
n = len(s)
dp = [[0] * (n + 1) for _ in range(n + 1)]
dp[0][-1] = 1
for i in range(n):
curr_char = s[i]
for prev in range(-1, i):
if dp[i][prev] == 0:
continue
dp[i + 1][prev] = (dp[i + 1][prev] + dp[i][prev]) % MOD
can_add = True
if prev != -1 and s[prev] == curr_char:
can_add = False
if can_add:
dp[i + 1][i] = (dp[i + 1][i] + dp[i][prev]) % MOD
result = 0
for prev in range(n):
result = (result + dp[n][prev]) % MOD
return result
使用了动态规划后,时间复杂度明显下降了
学习心得
这道题给了我很大的启发。一开始我用了最直观的方法:生成所有子序列然后一个个判断。这种暴力解法虽然容易想到,但完全不适合处理较大的输入。
通过与豆包的交流,我学到了在处理这类组合问题时,需要多思考如何利用问题的特殊性质。比如这道题中,一个好字符串的子序列一定也是好字符串,这个性质就可以用来优化算法。
动态规划的应用也让我印象深刻。原本我觉得这题跟动态规划不太相关,但通过豆包的提示,发现用dp来解决不仅代码更简洁,效率也大大提高了。这提醒我在解题时不要局限于某个特定的思路,要多角度思考。