好字符串子序列计数问题 | 豆包MarsCode AI刷题

102 阅读5分钟

问题描述

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

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

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

问题解决

对于这个问题,我有了如下思路:

  1. 首先需要理解什么是'好字符串':不包含长度≥2的回文子串
  2. 然后需要考虑如何生成所有子序列
  3. 对于每个子序列,需要判断它是否是'好字符串'
  4. 最后需要处理大数取模的问题

由此得出总体思路:先写一个函数判断字符串是否为好字符串,然后再考虑如何高效地生成和统计子序列。

基于此,我写出了第一版代码,是一个暴力解法:

    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来解决不仅代码更简洁,效率也大大提高了。这提醒我在解题时不要局限于某个特定的思路,要多角度思考。