区间动态规划例题:切棍子使每段都在指定长度集合的最小切割成本(进阶版:集合版本)
字数 3160 2025-12-12 03:50:23

区间动态规划例题:切棍子使每段都在指定长度集合的最小切割成本(进阶版:集合版本)

题目描述

你有一根长度为 n 的木棍,以及一个包含 m 个不同整数的集合 validLengths(集合中的每个整数都严格大于0且不超过 n)。你的目标是通过一系列切割,将木棍切成若干段,使得每一段的长度都恰好validLengths 集合中。每次切割的成本等于当前被切割木棍的长度。允许不进行任何切割(即,如果初始木棍的长度本身就在集合中,则成本为0)。你需要计算为了达成目标所需的最小总切割成本。如果无法达成目标(即不能通过切割使得所有段的长度都在集合中),则返回 -1

例如:

  • 输入:n = 7, validLengths = [2, 3]
  • 输出:最小切割成本为 7 + 4 = 11(或 7 + 3 = 10,取决于具体切割方案),但这里需要你计算出确切的最小值。
    解释:将长度为7的木棍切成段,每段长度必须是2或3。一种方案:先切成3和4(成本7),再将4切成2和2(成本4),最终得到[3,2,2],都在集合中。总成本为11。但可能存在更优方案。

解题思路

这是一个区间动态规划(Interval DP)问题。我们可以定义状态 dp[left][right] 表示将木棍的子区间 [left, right](长度为 right-left+1)切分成若干段,每段长度都在 validLengths 集合中的最小切割成本。注意,这里 leftright 不是原木棍上的物理位置,而是我们考虑的木棍的一个连续段,长度为 len = right-left+1。由于原木棍是连续的,我们只需要考虑长度 len,因此可以将状态简化为 dp[len],表示将一段长度为 len 的木棍切成合法段的最小切割成本。

关键点:

  1. 如果长度 len 本身就在 validLengths 中,那么可以不切割,成本为0。
  2. 否则,我们需要尝试所有可能的切割点 cutcut 表示从左端开始切割出的第一段长度,cut 必须在 validLengths 中),然后递归处理剩余部分 len-cut
  3. 每次切割的成本是当前木棍的长度 len(因为题目说成本等于被切割木棍的长度)。
  4. 这是一个典型的分割型DP:dp[len] = min_{cut in validLengths, cut < len} (len + dp[cut] + dp[len-cut]),但注意这里切割一次后,对左右两段都需要继续切割(如果它们还不是合法段)。但更直观的写法是:我们考虑在第一刀切割点 cut 后,左段 cut 和右段 len-cut 分别处理,且左右两段的切割成本是独立的,但当前这一刀的成本是 len
  5. 因此状态转移方程为:
    • 如果 lenvalidLengths 中:dp[len] = 0(可以不切)。
    • 否则:dp[len] = min_{cut in validLengths, cut < len} (len + dp[cut] + dp[len-cut])
    • 特别地,如果没有任何 cut 可选,则 dp[len] = INF(表示不可行)。
  6. 最终答案是 dp[n],如果为 INF 则返回 -1

详细步骤

假设 n=7, validLengths=[2,3]

  1. 状态定义
    dp[len]:将长度为 len 的木棍切成若干段,每段长度都在集合中的最小切割成本。len1n

  2. 初始化
    为了方便,我们用一个集合 validSet 存储 validLengths,便于快速判断一个长度是否合法。
    初始化 dp[0] = 0(长度为0不需要切割,但本题中长度至少为1,这只是为了方便)。
    对于 len1n,先设为 INF(一个很大的数,表示不可行)。

  3. 状态转移
    从小到大计算 dp[len](因为转移依赖更小的长度)。

    • 如果 lenvalidSet 中:dp[len] = 0(直接作为一段,不需要切割)。
    • 否则,枚举所有可能的切割点 cutcut 取自 validLengths,且 cut < len):
      那么剩余长度为 len-cut,必须满足 len-cut > 0
      如果 dp[cut]dp[len-cut] 都不是 INF,说明左右两段都可以切成合法段。
      此时总成本 = 当前切割成本 len + 左段的最小成本 dp[cut] + 右段的最小成本 dp[len-cut]
      取所有可能 cut 中的最小值作为 dp[len]
    • 如果没有任何可行的 cut,则 dp[len] 保持 INF
  4. 计算过程示例(n=7, valid=[2,3])
    令 INF = 一个大数如 10**9

    • len=1:不在集合中,且没有合法切割点(cut只能是2或3,但都>1),所以 dp[1]=INF。
    • len=2:在集合中,dp[2]=0。
    • len=3:在集合中,dp[3]=0。
    • len=4:不在集合中。尝试切割点 cut=2(合法):
      • 剩余长度=2,dp[2]=0,dp[2]=0。
      • 成本 = 4 + dp[2] + dp[2] = 4 + 0 + 0 = 4。
        尝试 cut=3(合法):
      • 剩余长度=1,dp[3]=0,但 dp[1]=INF,不可行。
        所以 dp[4] = min(4, INF) = 4。
    • len=5:不在集合中。尝试 cut=2:
      • 剩余长度=3,dp[2]=0,dp[3]=0,成本=5+0+0=5。
        尝试 cut=3:
      • 剩余长度=2,dp[3]=0,dp[2]=0,成本=5+0+0=5。
        所以 dp[5] = min(5,5) = 5。
    • len=6:不在集合中。尝试 cut=2:
      • 剩余长度=4,dp[2]=0,dp[4]=4,成本=6+0+4=10。
        尝试 cut=3:
      • 剩余长度=3,dp[3]=0,dp[3]=0,成本=6+0+0=6。
        所以 dp[6] = min(10,6) = 6。
    • len=7:不在集合中。尝试 cut=2:
      • 剩余长度=5,dp[2]=0,dp[5]=5,成本=7+0+5=12。
        尝试 cut=3:
      • 剩余长度=4,dp[3]=0,dp[4]=4,成本=7+0+4=11。
        所以 dp[7] = min(12,11) = 11。
  5. 最终结果
    dp[7] = 11,所以最小切割成本为11。返回11。

算法复杂度

  • 时间复杂度:O(n * m),其中 n 是木棍长度,m 是集合大小(因为对每个 len,我们尝试所有可能的合法切割长度)。
  • 空间复杂度:O(n) 用于存储 dp 数组。

思考扩展

  • 如果集合 validLengths 很大(比如包含很多长度),但 n 较小,直接枚举是可行的。
  • 如果 n 很大(比如10^5),但集合很小,这个DP算法仍然高效(O(n*m))。
  • 如果题目要求输出具体切割方案,我们可以额外记录每次选择的最优切割点,然后递归回溯。

代码实现(Python伪代码)

def minCutCost(n, validLengths):
    validSet = set(validLengths)
    INF = 10**9
    dp = [INF] * (n+1)
    dp[0] = 0  # 辅助,实际不会用到dp[0]
    for length in range(1, n+1):
        if length in validSet:
            dp[length] = 0
        else:
            for cut in validLengths:
                if cut < length:
                    if dp[cut] != INF and dp[length-cut] != INF:
                        dp[length] = min(dp[length], length + dp[cut] + dp[length-cut])
    return dp[n] if dp[n] != INF else -1

通过以上步骤,你可以理解如何将一个切棍子问题推广到给定长度集合的版本,并利用区间DP求解最小切割成本。

区间动态规划例题:切棍子使每段都在指定长度集合的最小切割成本(进阶版:集合版本) 题目描述 你有一根长度为 n 的木棍,以及一个包含 m 个不同整数的集合 validLengths (集合中的每个整数都严格大于0且不超过 n )。你的目标是通过一系列切割,将木棍切成若干段,使得每一段的长度都 恰好 在 validLengths 集合中。每次切割的成本等于当前被切割木棍的长度。允许不进行任何切割(即,如果初始木棍的长度本身就在集合中,则成本为0)。你需要计算为了达成目标所需的最小总切割成本。如果无法达成目标(即不能通过切割使得所有段的长度都在集合中),则返回 -1 。 例如: 输入: n = 7 , validLengths = [2, 3] 输出:最小切割成本为 7 + 4 = 11 (或 7 + 3 = 10 ,取决于具体切割方案),但这里需要你计算出确切的最小值。 解释:将长度为7的木棍切成段,每段长度必须是2或3。一种方案:先切成3和4(成本7),再将4切成2和2(成本4),最终得到[ 3,2,2 ],都在集合中。总成本为11。但可能存在更优方案。 解题思路 这是一个区间动态规划(Interval DP)问题。我们可以定义状态 dp[left][right] 表示将木棍的 子区间 [left, right] (长度为 right-left+1 )切分成若干段,每段长度都在 validLengths 集合中的最小切割成本。注意,这里 left 和 right 不是原木棍上的物理位置,而是我们考虑的木棍的一个 连续段 ,长度为 len = right-left+1 。由于原木棍是连续的,我们只需要考虑长度 len ,因此可以将状态简化为 dp[len] ,表示将一段长度为 len 的木棍切成合法段的最小切割成本。 关键点: 如果长度 len 本身就在 validLengths 中,那么可以不切割,成本为0。 否则,我们需要尝试所有可能的切割点 cut ( cut 表示从左端开始切割出的第一段长度, cut 必须在 validLengths 中),然后递归处理剩余部分 len-cut 。 每次切割的成本是当前木棍的长度 len (因为题目说成本等于被切割木棍的长度)。 这是一个典型的分割型DP: dp[len] = min_{cut in validLengths, cut < len} (len + dp[cut] + dp[len-cut]) ,但注意这里切割一次后,对左右两段都需要继续切割(如果它们还不是合法段)。但更直观的写法是:我们考虑在第一刀切割点 cut 后,左段 cut 和右段 len-cut 分别处理,且 左右两段的切割成本是独立的,但当前这一刀的成本是 len 。 因此状态转移方程为: 如果 len 在 validLengths 中: dp[len] = 0 (可以不切)。 否则: dp[len] = min_{cut in validLengths, cut < len} (len + dp[cut] + dp[len-cut]) 。 特别地,如果没有任何 cut 可选,则 dp[len] = INF (表示不可行)。 最终答案是 dp[n] ,如果为 INF 则返回 -1 。 详细步骤 假设 n=7 , validLengths=[2,3] 。 状态定义 dp[len] :将长度为 len 的木棍切成若干段,每段长度都在集合中的最小切割成本。 len 从 1 到 n 。 初始化 为了方便,我们用一个集合 validSet 存储 validLengths ,便于快速判断一个长度是否合法。 初始化 dp[0] = 0 (长度为0不需要切割,但本题中长度至少为1,这只是为了方便)。 对于 len 从 1 到 n ,先设为 INF (一个很大的数,表示不可行)。 状态转移 从小到大计算 dp[len] (因为转移依赖更小的长度)。 如果 len 在 validSet 中: dp[len] = 0 (直接作为一段,不需要切割)。 否则,枚举所有可能的切割点 cut ( cut 取自 validLengths ,且 cut < len ): 那么剩余长度为 len-cut ,必须满足 len-cut > 0 。 如果 dp[cut] 和 dp[len-cut] 都不是 INF ,说明左右两段都可以切成合法段。 此时总成本 = 当前切割成本 len + 左段的最小成本 dp[cut] + 右段的最小成本 dp[len-cut] 。 取所有可能 cut 中的最小值作为 dp[len] 。 如果没有任何可行的 cut ,则 dp[len] 保持 INF 。 计算过程示例 (n=7, valid=[ 2,3 ]) 令 INF = 一个大数如 10**9 。 len=1:不在集合中,且没有合法切割点(cut只能是2或3,但都>1),所以 dp[ 1 ]=INF。 len=2:在集合中,dp[ 2 ]=0。 len=3:在集合中,dp[ 3 ]=0。 len=4:不在集合中。尝试切割点 cut=2(合法): 剩余长度=2,dp[ 2]=0,dp[ 2 ]=0。 成本 = 4 + dp[ 2] + dp[ 2 ] = 4 + 0 + 0 = 4。 尝试 cut=3(合法): 剩余长度=1,dp[ 3]=0,但 dp[ 1 ]=INF,不可行。 所以 dp[ 4 ] = min(4, INF) = 4。 len=5:不在集合中。尝试 cut=2: 剩余长度=3,dp[ 2]=0,dp[ 3 ]=0,成本=5+0+0=5。 尝试 cut=3: 剩余长度=2,dp[ 3]=0,dp[ 2 ]=0,成本=5+0+0=5。 所以 dp[ 5 ] = min(5,5) = 5。 len=6:不在集合中。尝试 cut=2: 剩余长度=4,dp[ 2]=0,dp[ 4 ]=4,成本=6+0+4=10。 尝试 cut=3: 剩余长度=3,dp[ 3]=0,dp[ 3 ]=0,成本=6+0+0=6。 所以 dp[ 6 ] = min(10,6) = 6。 len=7:不在集合中。尝试 cut=2: 剩余长度=5,dp[ 2]=0,dp[ 5 ]=5,成本=7+0+5=12。 尝试 cut=3: 剩余长度=4,dp[ 3]=0,dp[ 4 ]=4,成本=7+0+4=11。 所以 dp[ 7 ] = min(12,11) = 11。 最终结果 dp[7] = 11 ,所以最小切割成本为11。返回11。 算法复杂度 时间复杂度:O(n * m),其中 n 是木棍长度, m 是集合大小(因为对每个 len ,我们尝试所有可能的合法切割长度)。 空间复杂度:O(n) 用于存储 dp 数组。 思考扩展 如果集合 validLengths 很大(比如包含很多长度),但 n 较小,直接枚举是可行的。 如果 n 很大(比如10^5),但集合很小,这个DP算法仍然高效(O(n* m))。 如果题目要求输出具体切割方案,我们可以额外记录每次选择的最优切割点,然后递归回溯。 代码实现(Python伪代码) 通过以上步骤,你可以理解如何将一个切棍子问题推广到给定长度集合的版本,并利用区间DP求解最小切割成本。