区间动态规划例题:最小切割成本构造回文分区(带最小分区成本与长度限制)
1. 问题描述
给定一个字符串 s 和一个整数 L 和一个整数 K,你需要将 s 分割成若干连续的非空子串(称为“分区”),使得:
- 每个分区的长度不超过
L(即每个分区的字符个数 ≤ L); - 整个字符串被分割成恰好
K个分区(即必须刚好切成 K 段); - 每个分区的字符可以重新排列,如果可以通过重新排列字符使该分区成为回文串,则该分区的成本为 0,否则该分区的成本为 1。
目标:找到一种分割方式,使得总成本最小,并输出这个最小成本。
如果无法在满足长度 ≤ L 且刚好 K 个分区的前提下完成分割,则返回 -1。
示例
s = "aabbc", L = 3, K = 2
可能的分割:
- ["aa", "bbc"] → "aa" 是回文,成本 0;"bbc" 重新排列为 "bcb" 是回文,成本 0;总成本 0。
- ["aab", "bc"] → "aab" → 可排为 "aba",是回文,成本 0;"bc" 可排为 "bc" 不是回文,成本 1;总成本 1。
所以最小成本是 0。
2. 思路分析
这个问题涉及:
- 区间定义:字符串的每个子串
s[i:j](左闭右开,索引从 0 开始),我们需要知道它是否能够通过重排成为回文串。 - 子问题:
dp[k][i]表示将前 i 个字符(即s[0:i])分割成 k 个分区的最小成本。 - 转移:最后一个分区是
s[j:i],其中j是上一个分区的结尾。那么:
dp[k][i] = min(dp[k-1][j] + cost[j][i]),其中j满足i - j ≤ L,且j ≥ k-1(因为前 k-1 个分区至少每个分区 1 个字符,所以 j ≥ k-1)。 - 初始化:
dp[1][i] = cost[0][i],前提是i ≤ L,否则不可行(用无穷大表示)。 - 答案:
dp[K][n],其中 n 是字符串长度。如果为无穷大,则返回 -1。
3. 判断子串能否通过重排成为回文串
一个字符串可以通过重排成为回文串的条件是:
最多一个字符出现奇数次,其余字符都出现偶数次。
因此,对每个子串 s[l:r],我们可以在 O(1) 时间内判断它的奇偶性状态。我们可以用前缀异或的方式(26 个字母用 26 位表示奇偶性),但这里更简单:
- 预处理每个区间
[l, r]的字符计数奇偶性。 - 但更高效的方法:用滑动窗口 + 位掩码。不过 n 最大可能 1000 左右,我们可以直接 O(26 * n^2) 预处理。
预处理 cost[l][r]:
cost[l][r] = 0 如果子串能重排为回文,否则为 1。
具体方法:
- 用数组
cnt[26]统计区间字符频率。 - 看奇数字符的个数是否 ≤ 1。
4. 动态规划状态转移方程
设 n = len(s),dp[k][i] 表示前 i 个字符分成 k 个分区的最小成本(i 从 1 到 n,k 从 1 到 K)。
转移:
dp[k][i] = min{ dp[k-1][j] + cost[j][i] }
其中 j 取值范围:
j < ii - j ≤ L(分区长度限制)j ≥ k-1(前 k-1 个分区至少 k-1 个字符)
初始化:
dp[1][i] = cost[0][i] 如果 i ≤ L,否则 dp[1][i] = INF(不可行)。
dp[k][i] 初始为 INF。
答案:dp[K][n] 如果是 INF 则返回 -1,否则返回其值。
5. 详细步骤拆解
步骤 1:预处理 cost 数组
n = len(s)
cost = [[0]*n for _ in range(n)]
for l in range(n):
cnt = [0]*26
odd_count = 0
for r in range(l, n):
idx = ord(s[r]) - ord('a')
cnt[idx] += 1
if cnt[idx] % 2 == 1:
odd_count += 1
else:
odd_count -= 1
if odd_count <= 1:
cost[l][r] = 0
else:
cost[l][r] = 1
步骤 2:初始化 DP
INF = 10**9
dp = [[INF]*(n+1) for _ in range(K+1)]
# k=1
for i in range(1, n+1):
if i <= L:
dp[1][i] = cost[0][i-1]
步骤 3:状态转移
for k in range(2, K+1):
for i in range(k, n+1): # 至少 k 个字符才能分成 k 段
for j in range(k-1, i): # 前一段结束在 j-1,下一段是 [j, i-1]
if i - j > L:
continue
if dp[k-1][j] < INF:
dp[k][i] = min(dp[k][i], dp[k-1][j] + cost[j][i-1])
步骤 4:得到结果
ans = dp[K][n]
if ans >= INF:
return -1
else:
return ans
6. 时间复杂度分析
- 预处理 cost:O(26 * n^2)
- DP 状态数:O(K * n)
- 每个状态转移:O(min(L, n))
总复杂度 O(26 * n^2 + K * n * L),在 n ≤ 1000, K ≤ n, L ≤ n 时可接受。
7. 举例验证
例:s = "aabbc", L=3, K=2
-
预处理 cost(0-based索引):
- cost[0][1] = "aa" → 奇数字符 0 → 0
- cost[0][4] = "aabbc" → 奇数字符 c:1 个 → 0
- cost[2][4] = "bc" → 奇数字符 b,c 各 1 → 2 → 1
等等。
-
dp[1][1] = cost[0][0]=0
dp[1][2] = cost[0][1]=0
dp[1][3] = cost[0][2]? "aab" → 奇数字符 b:1 → 0
等等。 -
计算 dp[2][5]:
- 从 j=1: dp[1][1]+cost[1][4] = 0+cost[1][4]="abbc" → 奇数字符 a,c? 检查:a:1,b:2,c:1 → 奇数字符 a,c → 2 → 1,总=1
- 从 j=2: dp[1][2]+cost[2][4]=0+1=1
- 从 j=3: dp[1][3]+cost[3][4]=0+1=1
- 从 j=4: dp[1][4]+cost[4][4]=0+0=0
所以 dp[2][5]=0,正确。
8. 边界情况
- 如果 K > n,不可能,返回 -1。
- 如果 L=0(实际上 L≥1 因为分区非空)。
- 如果字符串已经回文,成本也可能为 0。
- 预处理时注意索引对应(从 0 还是 1 开始容易错)。
9. 最终总结
本题综合了区间DP、子串回文性质判断、固定分区数的限制、长度限制。
核心技巧:
- 预处理区间是否能重排为回文(奇偶计数法)。
- DP 状态定义:
dp[分区数][前缀长度]。 - 在转移时加入长度 L 的限制。
你理解了吗?如果某个步骤有疑问,我可以再详细解释。