题目
一根X米长的树木,伐木工切割成不同长度的木材后进行交易,交易价格为每根木头长度的乘积。规定切割后的每根木头长度都为正整数;也可以不切割,直接拿整根树木进行交易。
请问伐木工如何尽量少的切割,才能使收益最大化?
输入描述
木材的长度(X ≤ 50)
输出描述
输出最优收益时的各个树木长度,以空格分隔,按升序排列
用例
| 输入 | 输出 |
|---|---|
| 10 | 3 3 4 |
思考
题目类似《算法导论》中介绍的钢条切割问题,使用动态规划求解。木头的长度是规模,长度为N的木头可以在1~N-1长度之间选择切还是不切,每个切割后的木头本身又是一个规模更小的子问题。因此选择自底向上不断迭代计算缓存规模较小的子问题,最终最长整个问题(规模为N)的解。
算法过程
-
初始化
- 当长度为 1 时,无法切割(切割后段长需为正整数),故:
dp[1] = 1(收益为自身长度)。counts[1] = 1(段数为 1,切割次数 0)。cuts[1] = [1](唯一方案)。
- 当长度为 1 时,无法切割(切割后段长需为正整数),故:
-
自底向上遍历长度 从长度 \(i = 2\) 到 \(i = X\),依次求解每个长度的最优解:
-
初始默认方案:不切割,直接取整根木材,故:
dp[i] = i(收益为自身长度)。counts[i] = 1(段数为 1)。cuts[i] = [i](仅自身一段)。
-
枚举切割点 对于每个长度 i,枚举所有可能的切割点 j(\(1 \leq j < i\)),将木材切分为长度 j 和 \(i-j\) 的两段:
- 子问题收益:两段的收益为
dp[j] * dp[i-j]。 - 段数:两段的总段数为
counts[j] + counts[i-j]。
- 子问题收益:两段的收益为
-
更新最优解
- 情况 1:新收益更大 若
dp[j] * dp[i-j] > dp[i],则更新:dp[i]为新收益。counts[i]为总段数。cuts[i]为两段的长度合并后排序的结果。
- 情况 2:收益相同但段数更少 若
dp[j] * dp[i-j] === dp[i]且总段数更少(counts[j] + counts[i-j] < counts[i]),则更新:counts[i]为更少的段数。cuts[i]为两段的长度合并后排序的结果。
- 情况 1:新收益更大 若
-
关键优化点
- 优先保证收益最大:在枚举切割点时,只有当新收益严格大于当前收益时,才无条件更新方案。
- 同等收益下最小化段数:当收益相同时,选择段数更少的方案(切割次数更少),例如:
- 长度为 4 时,方案
2+2(段数 2,乘积 4)和方案1+3(段数 2,乘积 3),前者段数相同但收益更高,故选择前者。 - 长度为 6 时,方案
3+3(段数 2,乘积 9)和方案2+2+2(段数 3,乘积 8),前者收益更高,故选择前者。
- 长度为 4 时,方案
该算法通过动态规划高效求解了最优切割方案,时间复杂度为 \(O(X^2)\),空间复杂度为 \(O(X)\),适用于 \(X \leq 50\) 的输入规模。
参考代码
function solution(n) {
let dp = Array(n+1).fill(0);
let counts = Array(n+1).fill(0);
let cuts = Array(n+1);
dp[1] = 1;
counts[1] = 1;
cuts[1] = [1];
for (let i = 2; i <= n; i++) {
dp[i] = i;
counts[i] = 1;
cuts[i] = [i];
for (let j = 1; j < i; j++) {
const k = i - j;
const profit = dp[j] * dp[k];
const count = counts[j] + counts[k]
if (profit > dp[i] ) {
dp[i] = profit;
counts[i] = count;
cuts[i] = [...cuts[j], ...cuts[k]].sort();
} else if (profit === dp[i]) {
if (count < counts[i]) {
counts[i] = count;
cuts[i] = [...cuts[j], ...cuts[k]].sort();
}
}
}
}
return cuts[n].join(' ');
}
3015

被折叠的 条评论
为什么被折叠?



