区间动态规划例题:最小切割成本构造回文分区(带最小分区成本与长度限制)
字数 2492 2025-12-17 19:05:10

区间动态规划例题:最小切割成本构造回文分区(带最小分区成本与长度限制)


1. 问题描述

给定一个字符串 s 和一个整数 L 和一个整数 K,你需要将 s 分割成若干连续的非空子串(称为“分区”),使得:

  1. 每个分区的长度不超过 L(即每个分区的字符个数 ≤ L);
  2. 整个字符串被分割成恰好 K 个分区(即必须刚好切成 K 段);
  3. 每个分区的字符可以重新排列,如果可以通过重新排列字符使该分区成为回文串,则该分区的成本为 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. 思路分析

这个问题涉及:

  1. 区间定义:字符串的每个子串 s[i:j](左闭右开,索引从 0 开始),我们需要知道它是否能够通过重排成为回文串。
  2. 子问题dp[k][i] 表示将前 i 个字符(即 s[0:i])分割成 k 个分区的最小成本。
  3. 转移:最后一个分区是 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)。
  4. 初始化dp[1][i] = cost[0][i],前提是 i ≤ L,否则不可行(用无穷大表示)。
  5. 答案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。

具体方法:

  1. 用数组 cnt[26] 统计区间字符频率。
  2. 看奇数字符的个数是否 ≤ 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 < i
  • i - 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

  1. 预处理 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
      等等。
  2. dp[1][1] = cost[0][0]=0
    dp[1][2] = cost[0][1]=0
    dp[1][3] = cost[0][2]? "aab" → 奇数字符 b:1 → 0
    等等。

  3. 计算 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、子串回文性质判断、固定分区数的限制、长度限制
核心技巧

  1. 预处理区间是否能重排为回文(奇偶计数法)。
  2. DP 状态定义:dp[分区数][前缀长度]
  3. 在转移时加入长度 L 的限制。

你理解了吗?如果某个步骤有疑问,我可以再详细解释。

区间动态规划例题:最小切割成本构造回文分区(带最小分区成本与长度限制) 1. 问题描述 给定一个字符串 s 和一个整数 L 和一个整数 K ,你需要将 s 分割成若干连续的非空子串(称为“分区”),使得: 每个分区的长度不超过 L (即每个分区的字符个数 ≤ L); 整个字符串被分割成恰好 K 个分区(即必须刚好切成 K 段); 每个分区的字符可以重新排列,如果可以通过重新排列字符使该分区成为回文串,则该分区的成本为 0,否则该分区的成本为 1。 目标:找到一种分割方式,使得 总成本最小 ,并输出这个最小成本。 如果无法在满足长度 ≤ L 且刚好 K 个分区的前提下完成分割,则返回 -1。 示例 可能的分割: [ "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 < i i - 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 数组 步骤 2:初始化 DP 步骤 3:状态转移 步骤 4:得到结果 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 的限制。 你理解了吗?如果某个步骤有疑问,我可以再详细解释。