切棍子的最小成本问题(基础版)
题目描述
有一根长度为 \(n\) 单位的木棍,标记了从 1 到 n 的刻度位置(左端为 0,右端为 n)。
给你一个整数数组 cuts,其中 cuts[i] 表示需要在这根木棍上的某个位置进行切割的位置(1 ≤ cuts[i] ≤ n-1)。
每次切割的成本等于当前被切割木棍的长度。
请你返回将木棍按指定位置全部切开所需的最小总成本。
注意:
- 每次只能选择一个位置进行切割,并且必须从该位置将当前木棍切成两根。
- 切割顺序会影响总成本。
- cuts 数组中的位置互不相同,并且已按升序排列(如果不是,可以预先排序)。
- 木棍必须最终在 cuts 中所有位置都被切开。
示例
输入:
n = 7, cuts = [1, 3, 4, 5]
输出:16
解释:
一种最优的切割顺序是:
- 先在位置 3 切割,成本是 7(当前棍子长度为 7)。
- 然后在位置 5 切割(此时有两段:0-3 和 3-7,选择 3-7 这段切割位置 5),成本是 4(这段长度 4)。
- 接着在位置 1 切割(0-3 这段切割位置 1),成本是 3。
- 最后在位置 4 切割(3-5 这段切割位置 4),成本是 2。
总成本 = 7 + 4 + 3 + 2 = 16。
为什么这是区间动态规划?
- 切割会不断将一段木棍分成更小的两段。
- 每段木棍的切割决策只依赖于这段的起点和终点,以及这段内部的切割点。
- 因此可以定义区间 dp[l][r] 表示在某个“虚拟木棍区间”上完成所有必须切割的最小成本。
但要注意:这里木棍的“区间”不是从 0 到 n 的原始位置,而是用 cuts 数组加上两端 0 和 n 构成的所有“关键点”的索引区间。
详细解题过程
1. 问题转换
原始木棍总长 n,必须切割的点是 cuts 里的位置。
我们先把 cuts 排序,然后在数组前后分别加入 0 和 n,得到一个新的数组 arr。
例如 n=7, cuts=[1,3,4,5] → 排序后就是 [1,3,4,5],加入两端 → arr = [0, 1, 3, 4, 5, 7]。
arr 的长度 m = len(cuts) + 2。
这样,arr[i] 就表示第 i 个关键点的位置。
问题转化为:
在区间 [arr[l], arr[r]] 这段木棍上,如果内部还有一些必须切割的点(即 arr 中索引在 (l, r) 之间的点),那么怎样切割成本最小。
为什么这样转换?
因为切割点都在 arr 中,任意一段木棍的长度 = arr[r] - arr[l],其内部需要切割的点就是 arr 索引在 l 和 r 之间的那些点。
2. 定义 dp 状态
令 dp[l][r] 表示在木棍区间 (arr[l], arr[r]) 这一段上,完成内部所有必须切割的最小成本。
注意:这里 l 和 r 是 arr 的索引,范围是 0 到 m-1。
最终答案是 dp[0][m-1],即整个木棍。
特别规定:如果区间 (l, r) 内部没有必须切割的点(即 r = l+1),则 dp[l][r] = 0,因为这段不需要再切了。
3. 状态转移
对于 dp[l][r],假设我们在内部选择一个切割点 k,其中 l < k < r,表示我们在位置 arr[k] 处切一刀。
这一刀将区间分成两段:
左段对应 (l, k)
右段对应 (k, r)
切割这一刀的成本是当前这段木棍的长度:arr[r] - arr[l]。
之后还要分别处理左右两段,左段最小成本是 dp[l][k],右段最小成本是 dp[k][r]。
所以总成本是:
\[\text{cost} = (arr[r] - arr[l]) + dp[l][k] + dp[k][r] \]
我们要在 l 和 r 之间所有可能的 k 中,选一个使总成本最小的。
转移方程:
\[dp[l][r] = \min_{l < k < r} \left( (arr[r] - arr[l]) + dp[l][k] + dp[k][r] \right) \]
如果区间内没有可选的 k(即 r == l+1),则 dp[l][r] = 0。
4. 递推顺序
区间 DP 常见顺序:按区间长度从小到大计算。
区间长度 len 从 2 到 m-1(因为 l 和 r 至少相差 2 才有内部切割点,len=2 时内部无点,dp=0 已初始化)。
对于每个长度 len,枚举左端点 l,右端点 r = l + len,然后枚举切割点 k 在 (l, r) 之间。
5. 算法步骤
- 排序 cuts(如果未排序)。
- 构建 arr = [0] + sorted(cuts) + [n]。
- 令 m = len(arr)。
- 初始化 dp 为 m×m 的二维数组,初始值为 0(因为当区间长度=2时 dp=0 是成立的,但其他情况后面计算会覆盖)。
- 对于长度 len 从 2 到 m-1:
- 对于 l 从 0 到 m-1-len:
- r = l + len
- 如果 len == 2,dp[l][r] = 0(内部无切割点,可跳过,因为初始就是0)
- 否则:
- 初始化 dp[l][r] = 无穷大
- 对于 k 从 l+1 到 r-1:
- 当前成本 = (arr[r] - arr[l]) + dp[l][k] + dp[k][r]
- 更新 dp[l][r] 为最小值
- 对于 l 从 0 到 m-1-len:
- 返回 dp[0][m-1]
举例验证
n=7, cuts=[1,3,4,5]
arr = [0,1,3,4,5,7] (m=6)
初始化 dp 6×6 全 0。
长度 len=3(区间内有一个切割点):
- l=0, r=2 → arr[0]=0, arr[2]=3, 内部切割点 k=1 (arr[1]=1)
dp[0][2] = (3-0) + dp[0][1] + dp[1][2] = 3 + 0 + 0 = 3 - l=1, r=3 → (1,4) 长度 3, 内点 k=2 → dp=3
- l=2, r=4 → (3,5) 长度 2, 内点 k=3 → dp=2
- l=3, r=5 → (4,7) 长度 3, 内点 k=4 → dp=3
- l=0, r=3 等更长区间后续会用到这些子结果。
继续计算 len=4,5,... 最终得到 dp[0][5] = 16,与示例一致。
复杂度分析
- 时间复杂度:O(m³),其中 m = cuts.length + 2。因为三层循环:区间长度、左端点、切割点。
- 空间复杂度:O(m²),存储 dp 表。
关键点小结
- 将 cuts 扩展为 arr 数组,将原问题转化为“在关键点序列的相邻区间上做区间 DP”。
- 状态 dp[l][r] 表示在木棍区间 (arr[l], arr[r]) 上完成所有内部切割的最小成本。
- 切割点 k 的选择对应将大区间分成两个小区间,加上当前段长度的成本。
- 按区间长度从小到大递推,确保子问题已解。
如果你理解了上述过程,可以尝试用代码实现,或者进一步思考:如果 cuts 数量很大(比如 100),O(m³) 可能较慢,是否存在优化方法?但在基础版中,这个解法是标准且清晰的。