最长公共子序列的变种:带字符出现次数限制的最长公共子序列(进阶版:限制某些字符必须连续出现k次,且总出现次数有限制)
字数 1128 2025-11-16 14:19:59
最长公共子序列的变种:带字符出现次数限制的最长公共子序列(进阶版:限制某些字符必须连续出现k次,且总出现次数有限制)
我将为您详细讲解这个线性动态规划问题。这个问题结合了多种限制条件,是经典最长公共子序列问题的一个有趣变种。
问题描述
给定两个字符串s和t,以及以下限制条件:
- 某些特定字符在公共子序列中必须连续出现至少k次
- 每个字符在公共子序列中的总出现次数不能超过给定的限制
- 需要找到满足这些条件的最长公共子序列
示例:
s = "aabbccdd"
t = "aabbccdd"
限制:字符'a'必须连续出现至少2次,且总出现次数不超过3次
结果:长度为6("aabbcc"或"bbccdd"等)
解题思路
这个问题需要在经典LCS的基础上增加状态维度来跟踪各种限制条件。我们需要记录:
- 当前匹配位置
- 当前连续字符的出现次数
- 各字符的总出现次数
详细解题步骤
步骤1:状态定义
我们定义三维DP数组:
dp[i][j][state] = 长度
其中:
i:在字符串s中的位置(0 ≤ i ≤ len(s))j:在字符串t中的位置(0 ≤ j ≤ len(t))state:编码当前状态,包括:- 当前连续字符是什么
- 当前连续出现次数
- 各字符的总出现次数
由于状态空间可能很大,我们需要合理设计状态编码。
步骤2:状态压缩
为了减少状态空间,我们可以:
- 只跟踪当前连续字符和连续次数
- 使用哈希表记录各字符总出现次数
- 当连续次数超过k时,可以重置计数
状态可以设计为:
state = (last_char, consecutive_count, char_counts_tuple)
其中char_counts_tuple是各字符出现次数的元组表示。
步骤3:状态转移方程
对于每个状态(i, j, state),考虑三种情况:
情况1:匹配当前字符
如果s[i] == t[j],且满足限制条件:
if 满足连续出现限制 and 满足总次数限制:
new_state = 更新状态
dp[i+1][j+1][new_state] = max(dp[i+1][j+1][new_state], dp[i][j][state] + 1)
情况2:跳过s的当前字符
dp[i+1][j][state] = max(dp[i+1][j][state], dp[i][j][state])
情况3:跳过t的当前字符
dp[i][j+1][state] = max(dp[i][j+1][state], dp[i][j][state])
步骤4:状态更新规则
当匹配字符c时:
- 如果
c == last_char:consecutive_count += 1- 如果
consecutive_count >= k,可以重置为0(因为已满足连续要求)
- 如果
c != last_char:- 检查前一个字符是否满足连续要求
- 重置
consecutive_count = 1 - 更新
last_char = c
同时更新字符c的总出现次数。
步骤5:边界条件
# 初始状态
dp[0][0][初始状态] = 0
# 初始状态表示:没有字符,连续次数为0,各字符出现次数为0
initial_state = (None, 0, (0, 0, ..., 0))
步骤6:结果提取
最终结果是所有dp[len(s)][len(t)][state]中的最大值,其中state必须满足所有限制条件。
完整算法代码
def constrained_lcs(s, t, char_constraints, max_counts):
"""
s, t: 输入字符串
char_constraints: 字典,{char: 最小连续出现次数}
max_counts: 字典,{char: 最大总出现次数}
"""
m, n = len(s), len(t)
# 状态编码:(last_char, consecutive_count, char_counts)
# 使用字典来存储状态,避免维度爆炸
dp = [{} for _ in range((m+1)*(n+1))]
def get_index(i, j):
return i * (n+1) + j
# 初始状态
initial_state = (None, 0, tuple(0 for _ in range(26)))
dp[get_index(0, 0)][initial_state] = 0
result = 0
for i in range(m+1):
for j in range(n+1):
idx = get_index(i, j)
for state, length in dp[idx].items():
last_char, cons_count, counts_tuple = state
counts = list(counts_tuple)
# 更新结果
if all(check_constraints(last_char, cons_count, counts, char_constraints, max_counts)):
result = max(result, length)
# 情况1:匹配字符(如果可能)
if i < m and j < n and s[i] == t[j]:
c = s[i]
c_idx = ord(c) - ord('a')
new_counts = counts.copy()
new_counts[c_idx] += 1
# 检查总次数限制
if new_counts[c_idx] <= max_counts.get(c, float('inf')):
if c == last_char:
new_cons_count = cons_count + 1
# 如果满足连续要求,可以重置
if new_cons_count >= char_constraints.get(c, 1):
new_cons_count = 0 # 重置,因为已满足要求
else:
# 检查前一个字符是否满足连续要求
if last_char is None or cons_count >= char_constraints.get(last_char, 1):
new_cons_count = 1
new_last_char = c
else:
continue # 不满足连续要求,跳过
new_state = (new_last_char, new_cons_count, tuple(new_counts))
new_idx = get_index(i+1, j+1)
dp[new_idx][new_state] = max(dp[new_idx].get(new_state, 0), length + 1)
# 情况2:跳过s的当前字符
if i < m:
new_idx = get_index(i+1, j)
dp[new_idx][state] = max(dp[new_idx].get(state, 0), length)
# 情况3:跳过t的当前字符
if j < n:
new_idx = get_index(i, j+1)
dp[new_idx][state] = max(dp[new_idx].get(state, 0), length)
return result
def check_constraints(last_char, cons_count, counts, char_constraints, max_counts):
"""检查状态是否满足所有约束"""
# 检查最后一个字符的连续要求
if last_char is not None and cons_count > 0:
if cons_count < char_constraints.get(last_char, 1):
return False
# 检查各字符的总次数限制
for char, max_count in max_counts.items():
c_idx = ord(char) - ord('a')
if counts[c_idx] > max_count:
return False
return True
复杂度分析
- 时间复杂度:O(m × n × S),其中S是状态数量
- 空间复杂度:O(m × n × S)
由于状态空间可能很大,这个算法适用于字符串长度较小或限制条件较简单的情况。对于更复杂的情况,可能需要使用启发式搜索或其他优化技术。
这个问题的核心在于如何巧妙地设计状态表示来捕捉所有的限制条件,同时在状态转移时正确更新这些状态信息。