切棍子的最小成本 图解题解
这道题到底在问什么
- 输入
- n=7, cuts=[1,3,4,5]
- 输出
- 16(枚举第一刀切哪个点,取拆出的两子段总成本最小的那种)
- 输入
- n=2, cuts=[1]
- 输出
- 2(只有一刀,成本就是整根长度 2)
先想最直接的笨办法
一句话套路:在每个区间里枚举「第一刀先切哪一刀」,这一刀把整段劈成左右两个更小的子区间。下面按区间从短到长逐格填这张 dp 表。(动画第 3 步)
最优解:为什么这么做
一句话答案:LeetCode 1547 切棍子的最小成本用区间 DP:每刀成本=当刀那段的长度,0 和 n 也当端点,dp[i][j]=切开这段的最少花费,枚举第一刀取小:dp[i][k]+dp[k][j]+段长,时间 O(c³)、空间 O(c²)。
一根木棍随便切,为什么顺序不同总价会差这么多
给一根长度 n 的木棍和切点清单 cuts,要把这些位置全切开。每刀成本等于当刀那段此刻的长度,切的先后自己定,求总成本的最小值。n=7、cuts=[1,3,4,5] 时答案 16。先切中间早断成短段、后面每刀便宜,先切两端则长段拖到最后反复付大数——难点全在顺序。
把所有切割顺序都试一遍,为什么棍子稍长就试不完
c 个切点排先后共 c! 种,c 才十几就多到枚举不完。且里面全是重复:剩下同一段、夹着同一批没切的点,往后最小成本就一样,却被反复重算。把『某段内部全切开』这个子问题算一次、存下来复用,就是动态规划(简称 DP)省浪费的办法。
把 0 和 n 也算成端点,dp[i][j] 到底存的是什么
把两端 0 和 n 也塞进切点,连同 cuts 排序,得端点数组 arr,本例 arr=[0,1,3,4,5,7] 共 6 个端点,任何一段都能用一对端点框出来。状态定成 dp[i][j](i、j 是 arr 下标,i 左 j 右,从 0 数起):切开 arr[i] 到 arr[j] 之间所有切点最少花多少。相邻端点间没夹切点,dp 值为 0;答案是整根的 dp[0][5]。『状态是一段区间、从短到长填表』的做法叫区间 DP。
为什么枚举『第一刀先切哪』,两段子成本一加就对了
填 dp[i][j] 只盯这段上『第一刀』先切哪个点。第一刀落下时整段还完整连着,必付一次段长 arr[j]-arr[i],与选哪个点无关。第一刀一落,整段劈成两截,各自内部正好是两个更小的子问题 dp[i][k] 和 dp[k][j],于是选点 k(k 取 i、j 之间的切点下标,i<k<j)的总成本是 dp[i][k]+dp[k][j]+(arr[j]-arr[i])。取遍候选 k 求最小,这就是转移式(由子区间成本推出这段成本)。
拿 n=7 的示例把三角表填到 16
相邻端点没夹切点的格填 0,再从短区间往长填。跨度 2(中间只夹 1 个切点)只有一个候选:dp[0][2]=0+0+(3-0)=3,dp[1][3]=3,dp[2][4]=2,dp[3][5]=3。跨度 3 试两候选取小:dp[0][3] 两个候选 0+3+4 与 3+0+4 都得 7;dp[1][4] 切 arr[2] 得 0+2+4=6;dp[2][5]=2+0+4=6。跨度 4:dp[0][4] 三候选 11、10、12 取 10;dp[1][5] 三候选都是 12。整根 dp[0][5] 段长 7:切 arr[1] 得 0+12+7=19、切 arr[2] 得 3+6+7=16、切 arr[3] 和 arr[4] 都得 17,最小 16 即答案。
段长若跟着第一刀一起变,dp 为什么会算出一堆虚高成本
常见写歪点是让段长随第一刀变——可第一刀切的是还没断开的整段,段长只认两端,让它随切点变,每个候选都加错数,整张表一路歪。边界:只有一个切点时成本恒等于整根长度,如 n=2、cuts=[1] 一刀就是 2。复杂度:枚举区间 O(c²),每区间再枚举第一刀 O(c),合计时间 O(c³)(大 O 记号,描述规模变大时操作数怎么涨),空间 O(c²)。
▶ 动画逐步走查(共 63 步)——想跟着动画一帧帧对照就展开
- 3一句话套路:在每个区间里枚举「第一刀先切哪一刀」,这一刀把整段劈成左右两个更小的子区间。下面按区间从短到长逐格填这张 dp 表。
- 4先看表骨架:行列都对应排序后的端点 arr=[0, 1, 3, 4, 5, 7]。dp[i][j] 是「切开 arr[i] 到 arr[j] 之间所有切点」的最小成本。对角线、下方,以及相邻端点(j 比 i 大 1)这些格子里都没有内部切点,成本 0,已经直接填好;只有上三角里跨度更大的「·」还没算,我们从最短的区间开始填。
- 5轮到 dp[0][2](紫格)。这段棍子从 0 到 3,长度 3。不管第一刀切在哪,此刻整段还连着,切它都得付一次段长 3;我们试遍每个候选 k,看哪种劈法两侧子区间加起来最省。
- 6试「第一刀先切 arr[1]=1」:它把整段劈成左段 dp[0][1]=0 和右段 dp[1][2]=0(两个蓝色依赖格),再加上这一刀的段长 3,候选总成本 3。
- 7候选 3 刷新了最优,暂定 dp[0][2]=3,对应「第一刀先切 arr[1]=1」。
- 8区间内所有候选 k 试完,dp[0][2]=3 锁定(变绿)。它代表把 0..3 之间的切点全切开的最小成本,更长的区间会直接拿它当子结果。
- 9轮到 dp[1][3](紫格)。这段棍子从 1 到 4,长度 3。不管第一刀切在哪,此刻整段还连着,切它都得付一次段长 3;我们试遍每个候选 k,看哪种劈法两侧子区间加起来最省。
- 10试「第一刀先切 arr[2]=3」:它把整段劈成左段 dp[1][2]=0 和右段 dp[2][3]=0(两个蓝色依赖格),再加上这一刀的段长 3,候选总成本 3。
- 11候选 3 刷新了最优,暂定 dp[1][3]=3,对应「第一刀先切 arr[2]=3」。
- 12区间内所有候选 k 试完,dp[1][3]=3 锁定(变绿)。它代表把 1..4 之间的切点全切开的最小成本,更长的区间会直接拿它当子结果。
- 13轮到 dp[2][4](紫格)。这段棍子从 3 到 5,长度 2。不管第一刀切在哪,此刻整段还连着,切它都得付一次段长 2;我们试遍每个候选 k,看哪种劈法两侧子区间加起来最省。
- 14试「第一刀先切 arr[3]=4」:它把整段劈成左段 dp[2][3]=0 和右段 dp[3][4]=0(两个蓝色依赖格),再加上这一刀的段长 2,候选总成本 2。
- 15候选 2 刷新了最优,暂定 dp[2][4]=2,对应「第一刀先切 arr[3]=4」。
- 16区间内所有候选 k 试完,dp[2][4]=2 锁定(变绿)。它代表把 3..5 之间的切点全切开的最小成本,更长的区间会直接拿它当子结果。
- 17轮到 dp[3][5](紫格)。这段棍子从 4 到 7,长度 3。不管第一刀切在哪,此刻整段还连着,切它都得付一次段长 3;我们试遍每个候选 k,看哪种劈法两侧子区间加起来最省。
- 18试「第一刀先切 arr[4]=5」:它把整段劈成左段 dp[3][4]=0 和右段 dp[4][5]=0(两个蓝色依赖格),再加上这一刀的段长 3,候选总成本 3。
- 19候选 3 刷新了最优,暂定 dp[3][5]=3,对应「第一刀先切 arr[4]=5」。
- 20区间内所有候选 k 试完,dp[3][5]=3 锁定(变绿)。它代表把 4..7 之间的切点全切开的最小成本,更长的区间会直接拿它当子结果。
- 21轮到 dp[0][3](紫格)。这段棍子从 0 到 4,长度 4。不管第一刀切在哪,此刻整段还连着,切它都得付一次段长 4;我们试遍每个候选 k,看哪种劈法两侧子区间加起来最省。
- 22试「第一刀先切 arr[1]=1」:它把整段劈成左段 dp[0][1]=0 和右段 dp[1][3]=3(两个蓝色依赖格),再加上这一刀的段长 4,候选总成本 7。
- 23候选 7 刷新了最优,暂定 dp[0][3]=7,对应「第一刀先切 arr[1]=1」。
- 24试「第一刀先切 arr[2]=3」:它把整段劈成左段 dp[0][2]=3 和右段 dp[2][3]=0(两个蓝色依赖格),再加上这一刀的段长 4,候选总成本 7。
- 25候选 7 没有更小,但同样达到 7,属于并列最优;表里保留先记录的那个,结果不变。
- 26区间内所有候选 k 试完,dp[0][3]=7 锁定(变绿)。它代表把 0..4 之间的切点全切开的最小成本,更长的区间会直接拿它当子结果。
- 27轮到 dp[1][4](紫格)。这段棍子从 1 到 5,长度 4。不管第一刀切在哪,此刻整段还连着,切它都得付一次段长 4;我们试遍每个候选 k,看哪种劈法两侧子区间加起来最省。
- 28试「第一刀先切 arr[2]=3」:它把整段劈成左段 dp[1][2]=0 和右段 dp[2][4]=2(两个蓝色依赖格),再加上这一刀的段长 4,候选总成本 6。
- 29候选 6 刷新了最优,暂定 dp[1][4]=6,对应「第一刀先切 arr[2]=3」。
- 30试「第一刀先切 arr[3]=4」:它把整段劈成左段 dp[1][3]=3 和右段 dp[3][4]=0(两个蓝色依赖格),再加上这一刀的段长 4,候选总成本 7。
- 31候选 7 没比当前最优 6 更省,这种劈法不划算,保留原最优。
- 32区间内所有候选 k 试完,dp[1][4]=6 锁定(变绿)。它代表把 1..5 之间的切点全切开的最小成本,更长的区间会直接拿它当子结果。
- 33轮到 dp[2][5](紫格)。这段棍子从 3 到 7,长度 4。不管第一刀切在哪,此刻整段还连着,切它都得付一次段长 4;我们试遍每个候选 k,看哪种劈法两侧子区间加起来最省。
- 34试「第一刀先切 arr[3]=4」:它把整段劈成左段 dp[2][3]=0 和右段 dp[3][5]=3(两个蓝色依赖格),再加上这一刀的段长 4,候选总成本 7。
- 35候选 7 刷新了最优,暂定 dp[2][5]=7,对应「第一刀先切 arr[3]=4」。
- 36试「第一刀先切 arr[4]=5」:它把整段劈成左段 dp[2][4]=2 和右段 dp[4][5]=0(两个蓝色依赖格),再加上这一刀的段长 4,候选总成本 6。
- 37候选 6 刷新了最优,暂定 dp[2][5]=6,对应「第一刀先切 arr[4]=5」。
- 38区间内所有候选 k 试完,dp[2][5]=6 锁定(变绿)。它代表把 3..7 之间的切点全切开的最小成本,更长的区间会直接拿它当子结果。
- 39轮到 dp[0][4](紫格)。这段棍子从 0 到 5,长度 5。不管第一刀切在哪,此刻整段还连着,切它都得付一次段长 5;我们试遍每个候选 k,看哪种劈法两侧子区间加起来最省。
- 40试「第一刀先切 arr[1]=1」:它把整段劈成左段 dp[0][1]=0 和右段 dp[1][4]=6(两个蓝色依赖格),再加上这一刀的段长 5,候选总成本 11。
- 41候选 11 刷新了最优,暂定 dp[0][4]=11,对应「第一刀先切 arr[1]=1」。
- 42试「第一刀先切 arr[2]=3」:它把整段劈成左段 dp[0][2]=3 和右段 dp[2][4]=2(两个蓝色依赖格),再加上这一刀的段长 5,候选总成本 10。
- 43候选 10 刷新了最优,暂定 dp[0][4]=10,对应「第一刀先切 arr[2]=3」。
- 44试「第一刀先切 arr[3]=4」:它把整段劈成左段 dp[0][3]=7 和右段 dp[3][4]=0(两个蓝色依赖格),再加上这一刀的段长 5,候选总成本 12。
- 45候选 12 没比当前最优 10 更省,这种劈法不划算,保留原最优。
- 46区间内所有候选 k 试完,dp[0][4]=10 锁定(变绿)。它代表把 0..5 之间的切点全切开的最小成本,更长的区间会直接拿它当子结果。
- 47轮到 dp[1][5](紫格)。这段棍子从 1 到 7,长度 6。不管第一刀切在哪,此刻整段还连着,切它都得付一次段长 6;我们试遍每个候选 k,看哪种劈法两侧子区间加起来最省。
- 48试「第一刀先切 arr[2]=3」:它把整段劈成左段 dp[1][2]=0 和右段 dp[2][5]=6(两个蓝色依赖格),再加上这一刀的段长 6,候选总成本 12。
- 49候选 12 刷新了最优,暂定 dp[1][5]=12,对应「第一刀先切 arr[2]=3」。
- 50试「第一刀先切 arr[3]=4」:它把整段劈成左段 dp[1][3]=3 和右段 dp[3][5]=3(两个蓝色依赖格),再加上这一刀的段长 6,候选总成本 12。
- 51候选 12 没有更小,但同样达到 12,属于并列最优;表里保留先记录的那个,结果不变。
- 52试「第一刀先切 arr[4]=5」:它把整段劈成左段 dp[1][4]=6 和右段 dp[4][5]=0(两个蓝色依赖格),再加上这一刀的段长 6,候选总成本 12。
- 53候选 12 没有更小,但同样达到 12,属于并列最优;表里保留先记录的那个,结果不变。
- 54区间内所有候选 k 试完,dp[1][5]=12 锁定(变绿)。它代表把 1..7 之间的切点全切开的最小成本,更长的区间会直接拿它当子结果。
- 55轮到 dp[0][5](紫格)。这段棍子从 0 到 7,长度 7。不管第一刀切在哪,此刻整段还连着,切它都得付一次段长 7;我们试遍每个候选 k,看哪种劈法两侧子区间加起来最省。
- 56试「第一刀先切 arr[1]=1」:它把整段劈成左段 dp[0][1]=0 和右段 dp[1][5]=12(两个蓝色依赖格),再加上这一刀的段长 7,候选总成本 19。
- 57候选 19 刷新了最优,暂定 dp[0][5]=19,对应「第一刀先切 arr[1]=1」。
- 58试「第一刀先切 arr[2]=3」:它把整段劈成左段 dp[0][2]=3 和右段 dp[2][5]=6(两个蓝色依赖格),再加上这一刀的段长 7,候选总成本 16。
- 59候选 16 刷新了最优,暂定 dp[0][5]=16,对应「第一刀先切 arr[2]=3」。
- 60试「第一刀先切 arr[3]=4」:它把整段劈成左段 dp[0][3]=7 和右段 dp[3][5]=3(两个蓝色依赖格),再加上这一刀的段长 7,候选总成本 17。
- 61候选 17 没比当前最优 16 更省,这种劈法不划算,保留原最优。
- 62试「第一刀先切 arr[4]=5」:它把整段劈成左段 dp[0][4]=10 和右段 dp[4][5]=0(两个蓝色依赖格),再加上这一刀的段长 7,候选总成本 17。
- 63候选 17 没比当前最优 16 更省,这种劈法不划算,保留原最优。
- 64区间内所有候选 k 试完,dp[0][5]=16 锁定(变绿)。它代表把 0..7 之间的切点全切开的最小成本,更长的区间会直接拿它当子结果。
- 65填到最右上角 dp[0][5]=16,它代表把整根棍子 0..7 的所有切点切开的最小总成本,就是最终答案 16。
⚠️ 容易写错的地方
✗ 错:忘了补两端、或切点没排序就直接 DP
✓ 对:arr = [0] + sorted(cuts) + [n] 补端点再排序
区间成本靠「端点之差」算,缺两端算不出真实段长;不排序会让 arr[j]-arr[i] 变负或错位
✗ 错:把这一刀的代价当成跟 k 有关
✓ 对:代价固定为 arr[j]-arr[i],与 k 无关
切「第一刀」时整段还连着,面对的就是整段,长度只取决于区间两端
✗ 错:C++/Java 忘了把 dp[i][j] 先设成大数
✓ 对:初始化 INT_MAX/2 再取 min
默认 0 会让 min 永远取到 0,得到错误的最小成本
完整代码(Python / C++ / Java)
Python
from typing import List
class Solution:
def minCost(self, n: int, cuts: List[int]) -> int:
arr = [0] + sorted(cuts) + [n]
m = len(arr)
dp = [[0] * m for _ in range(m)]
for length in range(2, m):
for i in range(m - length):
j = i + length
dp[i][j] = min(dp[i][k] + dp[k][j] + arr[j] - arr[i] for k in range(i + 1, j))
return dp[0][m - 1]C++
#include <algorithm>
#include <vector>
using namespace std;
class Solution {
public:
int minCost(int n, vector<int>& cuts) {
cuts.push_back(0); cuts.push_back(n);
sort(cuts.begin(), cuts.end());
int m = cuts.size();
vector<vector<int>> dp(m, vector<int>(m));
for (int len = 2; len < m; ++len) for (int i = 0; i + len < m; ++i) {
int j = i + len;
dp[i][j] = INT_MAX / 2;
for (int k = i + 1; k < j; ++k) dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j] + cuts[j] - cuts[i]);
}
return dp[0][m-1];
}
};Java
import java.util.*;
class Solution {
public int minCost(int n, int[] cuts) {
int m = cuts.length + 2;
int[] arr = new int[m];
arr[0] = 0; arr[m - 1] = n;
System.arraycopy(cuts, 0, arr, 1, cuts.length);
Arrays.sort(arr);
int[][] dp = new int[m][m];
for (int len = 2; len < m; len++) for (int i = 0; i + len < m; i++) {
int j = i + len;
dp[i][j] = Integer.MAX_VALUE / 2;
for (int k = i + 1; k < j; k++) dp[i][j] = Math.min(dp[i][j], dp[i][k] + dp[k][j] + arr[j] - arr[i]);
}
return dp[0][m - 1];
}
}复杂度
时间
O(c³)
区间端点数 c≈cuts 长度,枚举区间 O(c²)、每个区间再枚举一刀 O(c)
空间
O(c²)
二维 dp 表,大小 (c+2)×(c+2)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 切棍子的最小成本 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么只枚举『第一刀先切哪』就够,不会漏掉别的切法?+
因为任何一种把整段切完的方案,总有确定的『最先落下的那一刀』。按这一刀切在哪个点分类,每一类里第一刀都把整段劈成左右两个更小、互不干扰的子区间:子区间内部再怎么切都不影响对方,也不改变这第一刀的段长。把每个候选第一刀都试一遍,就覆盖了所有方案、且各类之间不重叠,所以取最小即全局最优。至于子区间内部的先后顺序,交给它们各自的 dp 递归去操心,一层层下去正好把整个切割顺序都排定了。LeetCode 312 戳气球是同款区间 DP:那题枚举『最后一个爆的气球』,这里枚举『第一刀』。
为什么切一段的成本只跟区间两端有关,而跟先切哪个点无关?+
关键在『第一刀』这个时点:这一刀落下时,整段木棍还是完整连着的、一个内部点都没断开,所以不论你选中间哪个点当第一刀,要切的都是同一段完整木棍,长度就是这段两端坐标之差 arr[j]-arr[i]。差别只发生在第一刀之后,它把整段断成两截,后续成本才落到两个子区间上,那部分才跟 k 有关。转移式就是把这两块分开记的:段长固定付一次,两段子成本随 k 变,各管各的。
复杂度只跟切点数有关、跟棍长 n 无关,这是为什么?+
dp 表的行列都是『端点』,端点数是切点数加 2(补上 0 和 n),所以表的规模只由切点个数 c 决定,跟 n 多大没关系。棍长 n 只是作为最后那个端点的坐标参与段长的减法,不会让表变大、也不会增加要枚举的区间或候选第一刀的数量。哪怕 n 有十亿,只要切点仍是十几个,dp 表还是那么小,O(c³) 照样跑得飞快。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 切棍子的最小成本 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。