带限制的最长回文子序列问题(每个字符最多使用一次)
问题描述
给定一个字符串 s,你需要从中找出一个最长的子序列,使得该子序列是回文的,并且每个字符在原始字符串 s 中最多只能被使用一次(即在构造回文子序列时,不能重复使用同一个字符位置,但可以从字符串的不同位置选取相同字符的多个出现,只要这些出现的位置不同即可。不过,这里通常指的是标准“子序列”定义,即从原串中按顺序取出字符但不要求连续,同一个位置不能重复使用。本问题的“限制”指的是:如果原串中某个字符出现了多次,你可以选取其中多个出现来构造回文子序列,但选取的每个位置必须是唯一的。这实际上就是标准子序列定义,并无额外限制。但本问题有一个常见变体:允许从原串中删除任意字符(每个位置最多删除一次)以得到最长回文子序列,求其长度。这就是经典的“最长回文子序列”问题。为了避免与列表中的“最长回文子序列的长度问题”重复,我将题目稍作改编,增加一个约束:在构造回文子序列时,原串中的每个字符只能被使用至多一次,且要求回文子序列中对称位置上的字符必须相同(这其实和经典定义一致)。为了区分,我将其明确为:求最长回文子序列的长度,但要求该回文子序列不能有重叠的字符选取(标准子序列本就无重叠)。为了增加挑战性,我们引入一个实际约束:字符串 s 中可能包含通配符 ‘?’,它可以匹配任意单个字符。现在,请你计算最长回文子序列的长度。
形式化定义
输入一个字符串 s,长度 n ≤ 1000。字符集为小写字母和 ‘?’。
‘?’ 可以当作任意一个小写字母来匹配。
你需要找到 s 的一个子序列(即从 s 中删除若干个字符后剩余的字符序列),使其为回文串,且长度最大。输出这个最大长度。
示例
输入:s = "a?b?a"
输出:4
解释:可以选取子序列 "a?b?a" 中删除一个字符得到 "ab?a" 或 "a?ba",但更优的是选取 "aba"(长度3)还是 "aa"(长度2)?实际上最长回文子序列是 "a??a",将两个 ‘?’ 视为相同字符(比如都视为 ‘a’),得到 "aaaa",但注意子序列必须保持原顺序,"a??a" 对应原串位置 0,1,3,4(字符 a, ?, b, a)中的 0,1,3,4,但位置1是 ‘?’,位置3是 ‘a’,若将 ‘?’ 匹配为 ‘a’,则子序列为 "a a a a",是回文。所以长度为4。
解题思路
这是一个区间动态规划问题。定义 dp[i][j] 表示在子串 s[i..j](闭区间)中,最长回文子序列的长度。目标是 dp[0][n-1]。
考虑如何从小区间推出大区间:
- 如果
i == j,即只有一个字符,那么它本身就是回文,长度为1。 - 如果
i > j,空区间,长度为0。 - 对于一般情况
i < j:- 如果
s[i] == s[j]或者s[i] == '?'或者s[j] == '?'或者 (s[i] == '?'且s[j] == '?')? 实际上,只要s[i]和s[j]可以匹配(即字符相等,或者至少一个是 ‘?’),那么我们可以将这两个字符作为回文子序列的首尾,然后加上中间部分的最长回文子序列长度,即dp[i][j] = dp[i+1][j-1] + 2。
但注意:当s[i]和s[j]都是 ‘?’ 时,它们可以匹配为同一个字符,所以也是+2。 - 如果
s[i]和s[j]不能匹配(即两个都是确定字符且不同),那么我们不能同时取这两个字符作为首尾。此时最长回文子序列可能在s[i+1..j]或s[i..j-1]中,所以dp[i][j] = max(dp[i+1][j], dp[i][j-1])。
- 如果
由于通配符 ‘?’ 的存在,匹配规则需要仔细处理:
匹配规则:s[i] 和 s[j] 可匹配的条件是:
s[i] == s[j],或s[i] == '?',或s[j] == '?'。
其实条件2和3已经包含了1(如果两字符相同,也满足其中一个为 ‘?’ 吗?不,所以必须单独列出相等条件)。更精确的写法:
match = (s[i] == s[j]) || (s[i] == '?') || (s[j] == '?')
当s[i] == '?'且s[j] == '?'时,显然match为真。
状态转移方程:
if i > j: dp[i][j] = 0
if i == j: dp[i][j] = 1
if i < j:
if match(s[i], s[j]):
dp[i][j] = dp[i+1][j-1] + 2
else:
dp[i][j] = max(dp[i+1][j], dp[i][j-1])
边界处理:注意当 i+1 > j-1 时(即区间长度为2时,i+1 == j 且 j-1 == i),dp[i+1][j-1] 对应空区间,值为0。
计算顺序:由于 dp[i][j] 依赖于 dp[i+1][j-1]、dp[i+1][j]、dp[i][j-1],即左下方、正下方、正左方。因此我们需要按区间长度从小到大计算:先算所有长度为1的区间,再算长度为2,直到长度为n。
详细步骤
- 读取字符串
s,记n = len(s)。 - 初始化二维数组
dp[n][n],全部置0。 - 处理长度为1的区间:对所有
i,dp[i][i] = 1。 - 从长度
len = 2到n循环:- 对于每个起点
i,终点j = i + len - 1(保证j < n)。 - 判断
s[i]和s[j]是否匹配:match = (s[i] == s[j]) or (s[i] == '?') or (s[j] == '?')。 - 如果匹配:
- 中间区间
dp[i+1][j-1]的长度:如果i+1 <= j-1,则取dp[i+1][j-1];否则为0(即当len==2时中间为空区间)。 dp[i][j] = dp[i+1][j-1] + 2。
- 中间区间
- 如果不匹配:
dp[i][j] = max(dp[i+1][j], dp[i][j-1])。
- 对于每个起点
- 最终答案:
dp[0][n-1]。
示例推演
s = "a?b?a"
n=5。
初始化 dp[i][i]=1。
长度2:
- i=0,j=1: s[0]=a, s[1]=? 匹配 → dp[0][1]=dp[1][0]+2,但dp[1][0]为空区间=0 → dp[0][1]=2。
- i=1,j=2: s[1]=?, s[2]=b 匹配 → dp[1][2]=dp[2][1]+2=0+2=2。
- i=2,j=3: s[2]=b, s[3]=? 匹配 → dp[2][3]=0+2=2。
- i=3,j=4: s[3]=?, s[4]=a 匹配 → dp[3][4]=0+2=2。
长度3:
- i=0,j=2: s[0]=a, s[2]=b 不匹配 → dp[0][2]=max(dp[1][2]=2, dp[0][1]=2)=2。
- i=1,j=3: s[1]=?, s[3]=? 匹配 → dp[1][3]=dp[2][2]+2=1+2=3。
- i=2,j=4: s[2]=b, s[4]=a 不匹配 → dp[2][4]=max(dp[3][4]=2, dp[2][3]=2)=2。
长度4:
- i=0,j=3: s[0]=a, s[3]=? 匹配 → dp[0][3]=dp[1][2]+2=2+2=4。
- i=1,j=4: s[1]=?, s[4]=a 匹配 → dp[1][4]=dp[2][3]+2=2+2=4。
长度5:
- i=0,j=4: s[0]=a, s[4]=a 匹配 → dp[0][4]=dp[1][3]+2=3+2=5? 等等,dp[1][3]=3,加2得5。但这是整个字符串,长度为5,似乎我们得到了整个字符串作为回文子序列?检查:子序列必须按顺序取,原串 "a?b?a",取全部5个字符,序列为 a,?,b,?,a,将两个 ‘?’ 都匹配为 ‘a’,得到 a,a,b,a,a,这不是回文(第二个字符a,倒数第二个字符a,但中间是b,对称位置分别是a和a、b和b?对称位置:位置0和4都是a,位置1和3都是a(‘?’ 匹配为a),位置2是b,对称位置是它自己,所以是回文:a a b a a 是回文吗?中心是b,两边对称:索引0和4都是a,索引1和3都是a,确实对称,所以是回文。因此最长回文子序列长度就是5,即整个字符串。
所以最终答案为5,但前面示例我写了4,那是错误估计。实际上整个字符串就是回文子序列(借助通配符)。
如果没有通配符,比如 s = "aabbaa",标准最长回文子序列是6(整个字符串就是回文)。本题因为有 ‘?’,可以灵活匹配,所以往往能得到更长的回文子序列。
复杂度分析
- 时间复杂度:O(n²),因为需要填充 O(n²) 的 dp 表,每个状态转移是 O(1)。
- 空间复杂度:O(n²),可以优化到 O(n)(只保留当前长度所需的状态),但实现简单起见用二维数组即可。
关键点
- 通配符 ‘?’ 的处理:在匹配判断时,只要一方是 ‘?’ 或两字符相同即可。
- 状态转移时,注意区间边界,当区间长度为2时,中间区间为空,长度为0。
- 最终结果就是整个字符串的最长回文子序列长度。
扩展思考
如果要求输出这个最长回文子序列本身(而不仅仅是长度),可以在 dp 过程中记录决策路径,然后回溯构造。这需要额外的空间存储选择方向。
通过这个题目,你不仅掌握了最长回文子序列的标准动态规划解法,还学会了如何处理通配符带来的灵活匹配,这是区间DP的一个有趣变种。