字节青训营-最大 UCC 子串编辑优化计算
Hakkali 2025.2.5
题目描述
小C拥有一个由字符 'U' 和 'C' 组成的字符串 S,并希望在编辑距离不超过给定值 m 的条件下,尽可能多地在字符串中获得 "UCC" 子串。
编辑距离定义为将字符串 S 转化为其他字符串时所需的最少编辑操作次数。允许的每次编辑操作为:插入、删除或替换单个字符。
要求:在允许最多 m 次编辑操作的前提下,编辑后的字符串中 "UCC" 子串的出现次数尽可能多。
测试样例
-
样例1
输入:m = 3, S ="UCUUCCCCC"
输出:3
说明:通过适当编辑,可以得到字符串"UCCUCCUCC"(例如 2 次替换),其中包含 3 个"UCC"子串。 -
样例2
输入:m = 6, S ="U"
输出:2
说明:可在后面插入"CCUCC"共 5 次插入操作(以及可能其他组合),使得最终字符串为"UCCUCC",包含 2 个"UCC"子串。 -
样例3
输入:m = 2, S ="UCCUUU"
输出:2
说明:例如替换最后 2 个字符,可以将字符串修改为"UCCUCC",从而获得 2 个"UCC"子串。
算法思想
本题要求在编辑操作(插入、删除、替换)允许的范围内,设计一种编辑方案使得得到的字符串中 "UCC" 子串的个数最大。
核心思路在于动态规划(DP)结合状态自动机来跟踪 "UCC" 的匹配进度,同时在 DP 状态中记录当前已经消耗的编辑操作数和累计获得的 "UCC" 匹配数。
状态定义
定义 DP 数组 dp,其中
- dp[i] 表示处理完原字符串前 i 个字符后(i 从 0 到 n),所有可能的状态集合。
- 每个状态用二元组 (st, cost) 表示:
- st:当前自动机状态,代表已匹配
"UCC"的进度。- 0 表示未匹配任何字符;
- 1 表示已匹配
'U'; - 2 表示已匹配
"UC",下一步如果读到'C'则可完成一个"UCC"子串。
- cost:从原字符串出发累计使用的编辑操作数。
- st:当前自动机状态,代表已匹配
对应状态的值 occ_val 为:
- 在达到该状态时,已累计完成的
"UCC"子串个数。
状态转移
1. 插入闭包
在处理每个位置 i 之前,我们先考虑在当前位置(不推进原字符串下标)是否能通过 插入操作改善状态。
- 插入操作:
- 可以选择插入
'U'或'C',每次插入增加编辑成本 1。 - 调用自动机转移函数
trans(st, ch),得到新状态 ns 和此次操作产生的"UCC"匹配增量 add_occ。
- 可以选择插入
- 利用一个 while 循环对当前 dp[i] 进行闭包更新,直到没有新的状态更新为止。
- 注意:插入操作不消耗原字符串中的字符,但会消耗编辑预算。
2. 推进原字符串
当在当前位置 dp[i] 的插入闭包结束后,如果未处理完原字符串(i < n),则考虑以下两种操作来消耗当前字符 S[i]:
- 删除操作
- 跳过 S[i],编辑成本 +1,自动机状态保持不变。
- 使用(匹配或替换)操作
- 选择输出一个字符。
- 如果 S[i] 与目标字符相同(目标字符可以为
'U'或'C'),则成本为 0; - 如果不同,则相当于替换操作,成本 +1。
- 如果 S[i] 与目标字符相同(目标字符可以为
- 根据选择的字符调用
trans(st, ch)得到新状态和匹配增量,更新累计匹配数。
- 选择输出一个字符。
3. 自动机转移函数 trans
函数 trans(st, ch) 定义如下:
- 状态 0(尚未匹配任何字符):
- 若读入
'U',转为状态 1; - 否则(读入
'C'),仍保持状态 0。
- 若读入
- 状态 1(已匹配
'U'):- 若读入
'U',仍保持状态 1(新的'U'可能作为后续匹配的起点); - 若读入
'C',转为状态 2。
- 若读入
- 状态 2(已匹配
"UC"):- 若读入
'U',转为状态 1; - 若读入
'C',完成"UCC"匹配,计数增量 +1,同时状态重置为 0。
- 若读入
解题步骤
-
初始化 DP
- 令 dp[0] 为字典,初始状态为:
dp[0][(0, 0)] = 0,表示还未处理任何字符、自动机处于状态 0、编辑成本为 0,且累计匹配数为 0。
- 令 dp[0] 为字典,初始状态为:
-
对每个位置 i(从 0 到 n)进行处理:
-
插入闭包
对 dp[i] 中的每个状态 (st, cost),如果当前 cost 小于 m,则尝试插入'U'和'C':- 新成本 new_cost = cost + 1(插入一次);
- 根据
trans(st, ch)得到新状态 ns 和匹配增量 add_occ; - 新累计匹配数 new_occ = occ_val + add_occ;
- 更新 dp[i][(ns, new_cost)](如果未记录或新值更优)。 使用 while 循环重复以上操作,直到一次遍历没有任何状态更新为止。
-
推进原字符串
如果 i < n,则对 dp[i] 中的每个状态 (st, cost) 考虑:- 删除 S[i]:
新成本 = cost + 1,状态保持 st,匹配数不变,更新 dp[i+1]。 - 使用 S[i]:
遍历目标字符'U'和'C':- 如果 S[i] 与目标字符相同,成本不变;否则成本 +1(替换操作);
- 调用
trans(st, ch)得到新状态 ns 和增量 add_occ; - 累计匹配数更新为 occ_val + add_occ;
- 更新 dp[i+1][(ns, new_cost)]
- 删除 S[i]:
-
完整代码
def solution(m: int, s: str) -> int:
def trans(st, ch):
"""
自动机转移函数:
输入:当前状态 st(0,1,2),以及读入字符 ch('U' 或 'C')
输出:新的状态 ns,以及此次操作增加的 "UCC" 匹配数 add_occ(要么0 要么1)
"""
if st == 0:
if ch == 'U':
return (1,0)
else:
return (0,0)
elif st == 1:
if ch == 'C':
return (2,0)
else:
return (1,0)
else st == 2:
if ch == 'C':
return (0,1)
else:
return (1,0)
n = len(s)
# dp[i] 为一个字典,键为 (state, cost),值为累计匹配 "UCC" 的次数
dp = [dict() for _ in range(n+1)]
dp[0][(0, 0)] = 0 # 初始状态:未处理字符,状态 0,编辑成本 0,匹配数 0
# 对于每个位置 i(末尾 i=n 允许进行插入操作, 但无法删除或替换)
for i in range(n+1):
# 1.插入闭包:在当前位置 i 不消耗原串字符的前提下反复尝试插入操作
while True:
updated = False
for (st, cost) in list(dp[i].keys()):
occ_val = dp[i][(st, cost)] # 即occrance value,表示'UCC'的出现次数
if cost < m:
for ch in ['U', 'C']:
new_cost = cost + 1
if new_cost > m:
continue
new_st, add_occ = trans(st, ch)
new_occ = occ_val + add_occ
key = (new_st, new_cost)
# 注意当操作不为插入时,下面三行中的i会变成i+1,至于为什么可以思考一下
if key not in dp[i] or dp[i][key] < new_occ:
dp[i][key] = new_occ
updated = True
if not updated:
break
# 如果 i 已到末尾,则只考虑插入,不考虑删除和替换
if i == n:
continue
# 处理原串字符 s[i],将 dp[i] 状态推进到 dp[i+1]
for (st, cost) in list(dp[i].keys()):
occ_val = dp[i][(st, cost)]
# 2. 删除操作:跳过 s[i],成本 +1,状态不变
new_cost = cost + 1
if new_cost <= m:
key = (st, new_cost)
if key not in dp[i+1] or dp[i+1][key] < occ_val:
dp[i+1][key] = occ_val
# 2. 使用 s[i]:匹配或替换操作,输出一个字符,可选 'U' 或 'C'
for ch in ['U', 'C']:
# 如果 s[i] 与 ch 相同,则无需替换,成本为 0;否则成本 +1
add_cost = 0 if s[i] == ch else 1
new_cost = cost + add_cost
if new_cost > m:
continue
ns, add_occ = trans(st, ch)
new_occ = occ_val + add_occ
key = (ns, new_cost)
if key not in dp[i+1] or dp[i+1][key] < new_occ:
dp[i+1][key] = new_occ
return max(dp[n].values())
if __name__ == '__main__':
print(solution(3, "UCUUCCCCC") == 3)
print(solution(6, "U") == 2)
print(solution(2, "UCCUUU") == 2)
解题心得
这道题的关键在于如何在有限的编辑距离预算内,通过合理的编辑操作最大化字符串中 "UCC" 子串的个数。解决过程中主要有两个核心思想:
-
状态自动机
使用一个简单的自动机来跟踪"UCC"的匹配进度(状态 0 → 1 → 2 → 完成匹配并重置)。这样,每一次字符输出(无论是来自原串、替换或插入)都可以通过trans函数更新状态和匹配数。 -
编辑距离下的动态规划
定义 DP 状态 (st, cost),在不同阶段记录自动机状态、已经使用的编辑操作数以及累计获得的匹配数。- 插入闭包:在同一位置上反复进行插入操作,充分利用剩余编辑预算来“制造”更多匹配机会。
- 删除和使用操作:同时考虑删除原串字符和利用原字符(匹配或替换)的可能性,保证状态能够向后推进。