引言
最开始拿到这道题知道是用动态规划来解决,但是思考了很久,发现仅仅使用一个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" 子串。
整体思路:
对于该题,思考的难点在于:
- 如何设计动归数组
- 如何存储有用的信息
为了解决以上问题,本题设计了两个动归数组:
'dp_match'
记录匹配‘UCC’
的编辑距离。用于记录从当前字符 s[i] 开始,匹配 “UCC” 子串的各个进度阶段的最小编辑距离。
2.
'dp'
主动态记录。用于记录前p个字符,经过q次操作后最大能匹配上的‘UCC’
的数量。
分别详细描述两个动归数组的作用流程:
'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_len
是dp_match
的列数,限制了匹配的最大长度;如果从第i
个字符开始匹配,为了匹配到UCC
最大的长度为match_len
。min(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)