最长回文子序列的变种:带字符替换限制的最长回文子序列(进阶版:替换操作有不同代价,且允许部分字符替换为通配符,同时限制某些字符必须连续出现)
字数 1508 2025-11-18 13:13:39
最长回文子序列的变种:带字符替换限制的最长回文子序列(进阶版:替换操作有不同代价,且允许部分字符替换为通配符,同时限制某些字符必须连续出现)
我将为您详细讲解这个线性动态规划问题。这个题目结合了最长回文子序列、字符替换代价和连续字符限制等多个概念。
题目描述
给定一个字符串s,我们可以进行以下操作:
- 删除任意字符(无代价)
- 替换字符为其他字符(有不同代价)
- 某些特定字符必须连续出现至少k次
目标是通过这些操作,找到最长的回文子序列,使得总代价不超过给定的预算B。
解题思路
这个问题可以分解为以下几个步骤:
步骤1:定义状态
我们定义dp[i][j][c][b]表示子串s[i...j]中,在预算b下,以字符c结尾且连续出现次数满足限制的最长回文子序列长度。
由于问题复杂,我们需要多个辅助状态:
- dp_len[i][j]: 子串s[i...j]的最长回文子序列长度
- dp_cost[i][j]: 达到该长度所需的最小代价
- dp_cont[i][j][c]: 以字符c结尾的连续字符信息
步骤2:基础情况
对于长度为1的子串:
- dp_len[i][i] = 1
- dp_cost[i][i] = 0(不需要任何操作)
- 对于所有字符c,如果s[i] == c,则dp_cont[i][i][c] = 1,否则为0
对于空子串(i > j):
- dp_len[i][j] = 0
- dp_cost[i][j] = 0
步骤3:状态转移方程
考虑子串s[i...j],我们有以下几种情况:
情况1:首尾字符相同
如果s[i] == s[j]:
- 不操作:dp_len[i][j] = dp_len[i+1][j-1] + 2
- 代价不变
情况2:首尾字符不同,考虑替换
我们可以选择:
- 将s[i]替换为s[j],代价为replace_cost[s[i]][s[j]]
- 将s[j]替换为s[i],代价为replace_cost[s[j]][s[i]]
- 删除s[i],代价为delete_cost
- 删除s[j],代价为delete_cost
取这些操作中能得到最长回文子序列且代价最小的方案。
情况3:处理连续字符限制
对于必须连续出现至少k次的字符,我们需要额外记录连续出现次数:
- 如果当前字符与之前选择的字符相同,且连续次数+1 >= k,则可以继续
- 否则需要重置连续计数或选择其他字符
具体的状态转移:
# 伪代码
for length from 2 to n:
for i from 0 to n-length:
j = i + length - 1
# 情况1:首尾字符相同
if s[i] == s[j]:
dp_len[i][j] = dp_len[i+1][j-1] + 2
new_cost = dp_cost[i+1][j-1]
# 更新连续字符信息
update_continuity(i, j, s[i])
# 情况2:考虑替换操作
else:
options = []
# 选项1:替换s[i]为s[j]
cost1 = replace_cost[s[i]][s[j]] + dp_cost[i+1][j-1]
if cost1 <= budget:
options.append((dp_len[i+1][j-1] + 2, cost1))
# 选项2:替换s[j]为s[i]
cost2 = replace_cost[s[j]][s[i]] + dp_cost[i+1][j-1]
if cost2 <= budget:
options.append((dp_len[i+1][j-1] + 2, cost2))
# 选项3:删除s[i]
cost3 = delete_cost + dp_cost[i+1][j]
if cost3 <= budget:
options.append((dp_len[i+1][j], cost3))
# 选项4:删除s[j]
cost4 = delete_cost + dp_cost[i][j-1]
if cost4 <= budget:
options.append((dp_len[i][j-1], cost4))
# 选择最优解
dp_len[i][j], dp_cost[i][j] = find_best_option(options)
步骤4:处理连续字符限制
对于必须连续出现至少k次的字符,我们需要额外的检查:
def check_continuity(i, j, char, current_count):
if char in must_continuous_chars:
if current_count + 1 < k: # 不满足连续要求
return False
return True
def update_continuity(i, j, char):
for prev_char in alphabet:
if prev_char == char:
# 连续字符计数增加
new_count = dp_cont[i+1][j-1][prev_char] + 1
if check_continuity(i, j, char, new_count):
dp_cont[i][j][char] = new_count
else:
# 重置连续计数
dp_cont[i][j][char] = 1
步骤5:处理通配符
如果允许通配符,我们需要考虑通配符可以匹配任何字符:
if s[i] == '*' or s[j] == '*':
# 通配符可以匹配任何字符
wildcard_options = []
for match_char in alphabet:
# 将通配符视为match_char
wildcard_cost = wildcard_cost if s[i] == '*' else 0
wildcard_cost += wildcard_cost if s[j] == '*' else 0
effective_cost = wildcard_cost + dp_cost[i+1][j-1]
if effective_cost <= budget:
wildcard_options.append((dp_len[i+1][j-1] + 2, effective_cost))
# 将通配符选项加入考虑
options.extend(wildcard_options)
步骤6:最终答案
最终答案是dp_len[0][n-1],其中n是字符串长度,且需要满足dp_cost[0][n-1] ≤ budget。
复杂度分析
- 时间复杂度:O(n² × |Σ| × B),其中n是字符串长度,|Σ|是字符集大小,B是预算
- 空间复杂度:O(n² × |Σ|)
示例说明
假设字符串s = "abcba",预算B = 3,必须连续字符={},替换代价矩阵为:
- a→b: 1, a→c: 2
- b→a: 1, b→c: 1
- c→a: 2, c→b: 1
计算过程:
- 基础情况:所有长度为1的子串都是回文
- 逐步计算更长的子串
- 最终找到"abcba"本身就是一个回文,不需要操作,长度为5
这个算法通过动态规划系统地考虑了所有可能的操作和约束条件,确保在预算范围内找到最长的回文子序列。