3472. 至多 K 次操作后的最长回文子序列
考点
- 区间DP
- 回文序列
思路
一、问题抽象
给定一个长度为 \(n\) 的小写字母串 \(s\),字符来自环状字母表 \(\{a,\dots,z\}\)。一次操作可以把某个字符变为其在字母表中的相邻字符(前一个或后一个),字母表首尾相连。
允许进行最多 \(K\) 次这样的操作,目标是:
在对字符串进行不超过 \(K\) 次操作后,得到的字符串中,最长回文子序列的长度最大是多少?
这里的“回文子序列”仅要求保持原相对顺序,不要求连续。
二、配对成本的建模
在任意回文子序列中,除去可能的中心字符,其余字符总是两两在左右对称的位置成对出现。因此,对每一对对称位置上的字符 \(s[i]\)、\(s[j]\),若想将其放入回文子序列,就需要把这两个字符改成相同的某个字母。
由于字母表是环状的,记字符 \(c_1, c_2\) 的索引差为 \(|c_1 - c_2|\),则这两个字符沿环相互靠拢的最少步数为: \[ \operatorname{dist}(c_1, c_2) = \min\bigl(|c_1 - c_2|,\ 26 - |c_1 - c_2|\bigr) \] 这个值就是:
“把 \(s[i]\) 和 \(s[j]\) 修改成同一个字母所需的最小操作次数”。
注意:最终它们被改成哪个字母完全不重要,只要两者相同即可。因此可以把修改过程压缩为一个局部成本 \(\operatorname{dist}(s[i], s[j])\)。
三、状态定义:恰好使用 \(k\) 次操作
设三维 DP 状态为: \[ f[k][i][j] \] 含义为:
在区间 \([i, j]\) 内,恰好使用 \(k\) 次操作(这些操作只发生在 \([i,j]\) 上),能得到的最长回文子序列长度。
其中:
- \(0 \le k \le K\),
- \(1 \le i \le j \le n\)。
需要特别说明的是:在这个定义下,“恰好使用 \(k\) 次操作”允许空耗操作,即可以在某个字符上做若干“多余”的循环修改(例如转一圈又转回来),不会改变回文长度。所以从最终答案的角度看,“恰好使用 \(k\) 次(允许空耗)”与“最多使用 \(k\) 次”是等价的。
四、边界条件
对于任意单个字符区间 \([i,i]\):
- 无论做多少(包括 0 次)操作,这个字符本身始终可以构成长度为 1 的回文子序列。
- 多出来的操作可以全部空耗在这个字符上。
因此有: \[ f[k][i][i] = 1 \quad \forall\ 0 \le k \le K,\ 1 \le i \le n. \]
五、状态转移
考虑区间 \([i, j]\)(其中 \(i < j\)),在该区间内恰好使用 \(k\) 次操作。设: \[ d = \operatorname{dist}(s[i], s[j]) = \min\bigl(|s[i] - s[j]|,\ 26 - |s[i] - s[j]|\bigr). \]
5.1 不将两端同时放入回文(跳过端点)
可以选择不把位置 \(i\) 纳入回文,只在 \([i+1, j]\) 中构造回文;也可以选择不把位置 \(j\) 纳入回文,只在 \([i, j-1]\) 中构造回文。这两种选择都不必消耗新操作,只是把“恰好 \(k\) 次操作”的使用全部留给子区间。
因此有转移: \[ f[k][i][j] \ge \max\bigl(f[k][i+1][j],\ f[k][i][j-1]\bigr). \] 这里的语义是:
在 \([i,j]\) 区间内恰好使用 \(k\) 次操作时,如果决定不使用端点 \(i\)(或 \(j\))构造回文,就把这 \(k\) 次操作全留给内部区间 \([i+1,j]\)(或 \([i,j-1]\))去使用或空耗。
5.2 将两端字符配成一对
若希望把 \(s[i]\) 和 \(s[j]\) 作为回文子序列的一对对称字符,则需要:
- 在区间 \([i,j]\) 内的操作总数为 \(k\);
- 其中有 \(d\) 次操作用于让 \(s[i]\) 和 \(s[j]\) 变成同一个字符;
- 剩余 \(k-d\) 次操作可以用于中间区间 \([i+1, j-1]\) 内(包括必要修改和空耗)。
只要 \(k \ge d\),就可以这样配对两端,因此得到: \[ f[k][i][j] \ge f[k - d][i+1][j-1] + 2. \] 无论 \(s[i]\) 与 \(s[j]\) 最初是否相等,这一式统一处理了“配成一对”的情况:
- 若本来相等,则 \(d = 0\),不需要额外操作;
- 若本来不等,则 \(d > 0\),需要消耗相应的操作次数。
5.3 综合转移方程
综合“跳过端点”和“两端配对”两种决策,对于每个 \(k, i, j\) 有: \[ f[k][i][j] = \max\left( f[k][i+1][j],\ f[k][i][j-1],\ \mathbf{1}_{k \ge d}\cdot\bigl(f[k-d][i+1][j-1] + 2\bigr) \right), \] 其中 \(d = \operatorname{dist}(s[i], s[j])\)。
在实现中,可以写成:
- 先用
max(f[k][i+1][j], f[k][i][j-1])初始化; - 若
k >= d,再尝试用f[k-d][i+1][j-1] + 2更新。
六、遍历顺序
要保证所有被依赖的子状态已经计算完毕,典型做法是:
- 先处理长度为 1 的区间,已经在初始化中完成;
- 对每个操作次数 \(k = 0, 1, \dots,
K\):
- 对区间长度
len = 2, 3, ..., n:- 枚举左端点
i,令j = i + len - 1:- 按上述转移更新
f[k][i][j]。
- 按上述转移更新
- 枚举左端点
- 对区间长度
在这个顺序下:
f[k][i+1][j]与f[k][i][j-1]来自同一个 \(k\) 和更短的区间;f[k-d][i+1][j-1]来自更小的 \(k-d\) 和更短的区间;- 均已经被计算过,因此是合法的拓扑顺序。
七、从“恰好 \(k\) 次”到“最多 \(K\) 次”
由于状态定义允许在任意字符上空耗操作:
- 对于任意在区间 \([1,n]\) 内使用不超过 \(k\) 次有效修改的方案,都可以通过额外添加若干空耗操作,使其变成“恰好使用 \(k\) 次”的方案,而不改变回文长度。
因此,在全局区间 \([1,n]\) 上,“恰好 \(k\) 次”所能达到的最优值与“最多 \(k\) 次”所能达到的最优值是一致的,只是数值上它们被记录在不同的 \(k\) 层上。
最终答案取: \[ \max_{0 \le k \le K} f[k][1][n] \] 即在所有“恰好使用 \(k\) 次操作(含空耗)”的方案中,选择最长回文子序列长度的最大值,这与题目要求的“最多 \(K\) 次操作”是等价的。
八、时间与空间复杂度
状态数量为: \[ O\bigl((K+1) \cdot n^2\bigr) \]
每个状态的转移是 \(O(1)\)。
因此时间复杂度为: \[ O(K n^2) \]
空间复杂度为: \[ O(K n^2) \]
九、AC代码
下面是对应上述建模的 C++ 实现,基于题意约束采用三维数组,状态语义为“在 \([i,j]\) 内恰好使用 \(k\) 次操作(允许空耗)所得的 LPS 长度”。
1 | class Solution { |