区间动态规划例题:切棍子使每段都在指定长度集合的最小切割成本(进阶版:集合版本)
题目描述
你有一根长度为 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。
- 剩余长度=3,dp[2]=0,dp[3]=0,成本=5+0+0=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。
- 剩余长度=4,dp[2]=0,dp[4]=4,成本=6+0+4=10。
- 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[2]=0,dp[5]=5,成本=7+0+5=12。
-
最终结果
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求解最小切割成本。