3628. 插入一个字母的最大子序列数
考点
- 状态机DP
- 前后缀分解
思路
题意
给定一个由大写字母组成的字符串
s,允许在任意位置插入至多一个大写字母,使最终字符串中子序列
“LCT” 的数量最大。子序列指按下标递增选取字符,不要求连续。
本讲义只关注字符
L、C、T;其余字符对计数无贡献,可忽略。
一、关键易错点:不能用“两层 DP(插入/不插入)+ 只保留一个最优态”来做
很多状态机 DP 会尝试维护两层状态:
- 未插入:
(L0, LC0, LCT0) - 已插入:
(L1, LC1, LCT1),并在遍历过程中只保留一个“最优”的已插入状态(例如优先最大化LCT,再最大化LC,再最大化L)。
这在本题会导致错误,其根本原因是:
- 插入
T的收益只依赖前缀LC; - 插入
L的收益只依赖后缀CT; - 插入
C的收益依赖 前缀 L 的数量与后缀 T 的数量的乘积,即位置相关的L_prefix × T_suffix。
已插入状态若只记录
(L, LC, LCT),并在遍历中剪枝丢掉其它候选,会丢失“插入位置对应的后缀
T
数量”信息,导致后续无法达到最优。也就是说,该问题的“已插入一次”状态并不满足常见的支配剪枝条件,不能安全压缩成一个三元组最优态。
因此,本题的可靠结构是:
- 先用一次遍历计算原串中
LCT的数量(记为base); - 再分别计算插入
L、C、T带来的LCT增量,取最大; - 返回
base + 增量最大值。
二、插入 L / C / T
各自带来的增量
设插入位置为某个边界(插在某个字符之前)。只需比较三种插入选择的最优增量。
1)插入 T 的增量
把 T 插到最右边最优。新 T
可以与左侧所有子序列 “LC” 组合成新的 “LCT”。
- 增量 = 全串中子序列 “LC” 的数量。
- 记为
LC_total。
2)插入 L 的增量
把 L 插到最左边最优。新 L
可以与右侧所有子序列 “CT” 组合成新的 “LCT”。
- 增量 = 全串中子序列 “CT” 的数量。
- 记为
CT_total。
3)插入 C 的增量
若把 C 插在某个边界处,新产生的 “LCT” 必须由:
- 左侧选一个
L; - 右侧选一个
T;
因此该边界的增量为:L_left * T_right,取所有边界的最大值即可
- 记为
bestInsertC = max_over_boundaries(L_left * T_right)
三、LCT(原串
base)如何一次遍历得到
使用经典子序列计数状态机,维护三个变量:
l:当前前缀中'L'的数量(即子序列 “L” 的数量)lc:当前前缀中子序列 “LC” 的数量lct:当前前缀中子序列 “LCT” 的数量
从左到右扫描字符 ch:
- 若
ch == 'L':l += 1 - 若
ch == 'C':lc += l(每个已有的 L 都可与该 C 组成一个 LC) - 若
ch == 'T':lct += lc(每个已有的 LC 都可与该 T 组成一个 LCT) - 其它字符:不变
扫描结束时,lct 即为原串 base。
四、一次扫描同时算出
base 与三种增量(对应 AC 代码)
下面代码以状态机方式在一次遍历中同时计算:
lct:原串baselc:LC_total(插 T 的增量)ct:CT_total(插 L 的增量)mx:bestInsertC(插 C 的增量)
其中 t 表示“尚未扫描到的后缀中 T
的数量”,用于实时计算 l * t 并取最大。
1 | class Solution { |