最大UCC子串计算问题求解思路记录 | 豆包MarsCode AI刷题

202 阅读6分钟

引言

最开始拿到这道题知道是用动态规划来解决,但是思考了很久,发现仅仅使用一个dp很难求解。于是在参考了答案后,再次解题,期间思路还是比较混乱,因此本篇笔记用来记录这道题的思路,供日后复习。

问题描述

小S有一个由字符 'U' 和 'C' 组成的字符串 SS,并希望在编辑距离不超过给定值 mm 的条件下,尽可能多地在字符串中找到 "UCC" 子串。

编辑距离定义为将字符串 SS 转化为其他字符串时所需的最少编辑操作次数。允许的每次编辑操作是插入、删除或替换单个字符。你需要计算在给定的编辑距离限制 mm 下,能够包含最多 "UCC" 子串的字符串可能包含多少个这样的子串。

例如,对于字符串"UCUUCCCCC"和编辑距离限制m = 3,可以通过编辑字符串生成最多包含3个"UCC"子串的序列。


测试样例

样例1:

输入:m = 3,s = "UCUUCCCCC"
输出:3

样例2:

输入:m = 6,s = "U"
输出:2

样例3:

输入:m = 2,s = "UCCUUU"
输出:2

解释

样例1:可以将字符串修改为 "UCCUCCUCC"(2 次替换操作,不超过给定值 m = 3),包含 3 个 "UCC" 子串。

样例2:后面插入 5 个字符 "CCUCC"(5 次插入操作,不超过给定值 m = 6),可以将字符串修改为 "UCCUCC",包含 2 个 "UCC" 子串。

样例3:替换最后 2 个字符,可以将字符串修改为 "UCCUCC",包含 2 个 "UCC" 子串。

整体思路:

对于该题,思考的难点在于:

  1. 如何设计动归数组
  2. 如何存储有用的信息

为了解决以上问题,本题设计了两个动归数组:

  1. 'dp_match'记录匹配 ‘UCC’ 的编辑距离。用于记录从当前字符 s[i] 开始,匹配 “UCC” 子串的各个进度阶段的最小编辑距离。

image.png 2. 'dp'主动态记录。用于记录前p个字符,经过q次操作后最大能匹配上的‘UCC’的数量。 image.png

分别详细描述两个动归数组的作用流程:

'dp_match'

dp_match 是一个 2D 数组,大小为 4 x (match_len + 1),其中:

  • 行数 4 对应匹配进度:0 表示匹配空字符串,1 表示匹配 “U”、2 表示匹配 “UC”、3 表示匹配 “UCC”。
  • 列数 max_len + 1 对应从当前字符开始,匹配到的字符数的可能值(包括零长度匹配)。
        match_len = min(m+3,n-i)
        dp_match = [[float('inf')] * (match_len+1) for _ in range(4)]
        dp_match[0][0]=0

其中match_lendp_match的列数,限制了匹配的最大长度;如果从第i个字符开始匹配,为了匹配到UCC最大的长度为match_lenmin(n - i, 3 + m) 既确保了匹配的长度不会超过剩余字符数,又考虑了m作为最大编辑次数的限制。3+m指的是UCC长度为3,能进行m次操作(可以理解为能删除m个字符),那么最大匹配限度是3+m。同时,还要确保其长度不超过字符串s的长度,所以min(n - i, 3 + m)

dp_match[0][0]初始化为0,表示初始编辑距离为0

            for p in range(4):
            for q in range(match_len+1):
                if dp_match[p][q] > m:
                    continue
                if p<3 and q<match_len: #替换or保留
                    if s[i+q] == 'UCC'[p]:
                        cost = 0
                    else:
                        cost = 1
                    dp_match[p+1][q+1] = min(dp_match[p+1][q+1],dp_match[p][q]+cost)
                if p<3: #插入
                    dp_match[p+1][q] = min(dp_match[p+1][q],dp_match[p][q]+1)
                if q<match_len: #删除
                    dp_match[p][q+1] = min(dp_match[p][q+1],dp_match[p][q]+1)

dp_match[p][q]代表从字符[i]开始,已匹配的进度为p,且已经匹配了q个字符时的最小编辑距离。

这个数组通过遍历字符并计算编辑距离来更新。例如:

  • 保留/替换:如果当前字符 s[i+q] 和目标字符串 “UCC” 的字符相等,则不需要编辑(成本为 0);否则,替换的成本为 1
  • 插入:增加字符以继续匹配 “UCC” 中的字符,插入操作的成本是 1
  • 删除:跳过当前字符并继续匹配,删除操作的成本是 1
            for q in range(match_len+1):
                c = dp_match[3][q]
                match_info[i].append((c,q))

dp_match[3][q]代表从[i]开始到q位置时,匹配到UCC所需的最小编辑距离,并将其存入match_info[i]

dp

    dp = [[-1] * (m+1) for _ in range(n+1)]
    dp[0][0] = 0

dp是一个二维数组,大小为(n+1)*(m+1),其中(n+1)为字符串的长度(包括空字符串的情况),(m+1)为对应允许的最大编辑次数(包括编辑次数为0的情况)

    for p in range(n+1):
        for q in range(m+1):
            if dp[p][q]==-1:
                continue
            if p<n: #不尝试改动,直接跳过或者删除
                dp[p+1][q] = max(dp[p][q],dp[p+1][q])
                if q<=m-1:#删除
                    dp[p+1][q+1] = max(dp[p][q],dp[p+1][q+1])

            if p<n and match_info[p]:#匹配
                for c,l in match_info[p]:
                    if c+q<= m and l+p <= n:
                        dp[l+p][c+q] = max(dp[l+p][c+q],dp[p][q]+1)

dp[p][q]代表前p个字符,经过q次操作时,能匹配到UCC到最大匹配次数。 dp 数组的值通过以下方式更新:

  • 删除操作:删除字符后,编辑次数增加 1。
  • 匹配操作:从 match_info[i] 获取匹配的最小编辑距离和匹配长度,通过计算可以更新 dp[i + l][e + c],其中 l 是匹配的长度,c 是匹配的编辑距离。

最终解题

至此,两个dp数组能实现本题的要求,并返回正确的结果,完整的解题如下所示:

def solution(m: int, s: str) -> int:
    n = len(s)
    dp = [[-1] * (m+1) for _ in range(n+1)]
    dp[0][0] = 0
    match_info = [[] for _ in range(n)]

    for i in range(n):
        match_len = min(m+3,n-i)
        dp_match = [[float('inf')] * (match_len+1) for _ in range(4)]
        dp_match[0][0]=0
        for p in range(4):
            for q in range(match_len+1):
                if dp_match[p][q] > m:
                    continue
                if p<3 and q<match_len: #替换or保留
                    if s[i+q] == 'UCC'[p]:
                        cost = 0
                    else:
                        cost = 1
                    dp_match[p+1][q+1] = min(dp_match[p+1][q+1],dp_match[p][q]+cost)
                if p<3: #插入
                    dp_match[p+1][q] = min(dp_match[p+1][q],dp_match[p][q]+1)
                if q<match_len: #删除
                    dp_match[p][q+1] = min(dp_match[p][q+1],dp_match[p][q]+1)
        for q in range(match_len+1):
            c = dp_match[3][q]
            match_info[i].append((c,q))

    for p in range(n+1):
        for q in range(m+1):
            if dp[p][q]==-1:
                continue
            if p<n: #不尝试改动,直接跳过或者删除
                dp[p+1][q] = max(dp[p][q],dp[p+1][q])
                if q<=m-1:#删除
                    dp[p+1][q+1] = max(dp[p][q],dp[p+1][q+1])

            if p<n and match_info[p]:#匹配
                for c,l in match_info[p]:
                    if c+q<= m and l+p <= n:
                        dp[l+p][c+q] = max(dp[l+p][c+q],dp[p][q]+1)

    res = 0
    for i in range(m+1):
        res = max(res,dp[n][i])
    return res


if __name__ == '__main__':
    print(solution(m=3, s="UCUUCCCCC") == 3)
    print(solution(m=6, s="U") == 2)
    print(solution(m=2, s="UCCUUU") == 2)