题目描述

✅ 补充题 7. 木头切割问题

给定整数数组 woods 和正整数 k,其中 woods[i] 表示第 i 根木头的长度。

你可以切割这些木头,得到至少 k 段长度相同的小木段。小木段的长度必须为正整数,每一段都必须来自某一根原木;剩余部分可以丢弃,不能把不同木头拼接起来。

请返回小木段可以采用的最大长度。

示例 1:

输入:woods = [4,7,2,10,5], k = 5
输出:4
解释:各根木头分别能切出 1、1、0、2、1 段长度为 4 的木段,共 5 段。

提示:

  • 保证存在正整数长度的可行切法,即答案至少为 1。
  • 切出的段数超过 k 也满足要求。

题意分析

有若干根木头,要从中切出至少 k 段等长的小木段,求能采用的最大正整数段长。k 为正数,每段必须来自同一根原木,剩余零头可以丢弃,但不能跨木头拼接。

切出超过 k 段也满足要求,只需留下其中需要的部分。原题保证有解;代码还额外兼容无解输入:如果连长度为一都凑不出足够段数,就返回零。空数组或全部为零的木头也按这一扩展规则处理。

解法:二分答案找最大可行长度

核心思路

[!blue]

先固定候选段长 length。长度为 wood 的一根木头最多切出 wood / length 段,使用整数除法向下取整。分别计算每根的段数并求和,达到 k 就说明该长度可行。

段长增加时,每根能切出的段数只会减少或不变,所以可行长度形成从小到大的一段前缀。目标是这一段中最大的值,可以二分长度范围,无需反复尝试所有整数。

正长度下界是一,上界是最长原木,任何小段都不可能比来源木头更长。代码用 ans 保存已经验证过的最大可行长度,闭区间 [left, right] 则只表示尚未处理的候选,二者含义不同。

若中点可行,先把它记录到 ans,再向右寻找更长长度;若不可行,更长的也都不可行,令右界降到中点前一位。区间为空时,所有可能改善答案的长度都已处理,返回记录值。完全无解时,ans 保持零。

判定累计到足够段数即可结束,后续原木不会让数量变少。最后一个候选已经可行时直接返回,避免在最大整数长度处执行加一;Go 还在累加前比较尚缺段数,防止机器整数上界附近的段数相加溢出。

解题步骤

  1. 扫描木头得到最长长度,初始化 left = 1、ans = 0。
  2. 在闭区间非空时取中点,逐根计算能切出的整数段数。
  3. 达到 k 判为可行,保存中点,并继续检查更大的候选;已是最后一个候选则直接返回。
  4. 段数不足时,排除中点及更长长度。
  5. 搜索结束返回 ans。

代码实现

class Solution {
    public int woodCut(int[] woods, int k) {
        int right = 0;

        for (int wood : woods) {
            right = Math.max(right, wood);
        }

        int left = 1;
        int ans = 0;

        while (left <= right) {
            int mid = left + (right - left) / 2;

            if (canCut(woods, k, mid)) {
                // 记录已验证的可行长度,后续区间只处理尚未确定的候选
                ans = mid;

                // 最后一个候选已确认可行,直接结束,避免上界处继续加一
                if (mid == right) {
                    return ans;
                }

                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }

        return ans;
    }

    private boolean canCut(int[] woods, int k, int len) {
        long count = 0;

        for (int wood : woods) {
            // 每根木头分别整除,不能跨木头拼接零头
            count += wood / len;

            if (count >= k) {
                return true;
            }
        }

        return false;
    }
}
func woodCut(woods []int, k int) int {
    right := 0
    for _, wood := range woods {
        if wood > right {
            right = wood
        }
    }

    left, ans := 1, 0
    for left <= right {
        mid := left + (right-left)/2
        if canCutWood(woods, k, mid) {
            // 记录已验证的可行长度,后续区间只处理尚未确定的候选
            ans = mid
            // 最后一个候选已确认可行,直接结束,避免上界处继续加一
            if mid == right {
                return ans
            }
            left = mid + 1
        } else {
            right = mid - 1
        }
    }
    return ans
}

func canCutWood(woods []int, k, length int) bool {
    var count int64
    for _, wood := range woods {
        // 每根分别整除,先对比尚缺段数,避免累计相加溢出
        pieces := int64(wood / length)
        if pieces >= int64(k)-count {
            return true
        }
        count += pieces
    }
    return false
}

复杂度分析

  • 时间复杂度:$O(n\log(M+1))$,其中 $n$ 是原木数量,$M$ 是最长长度。初始扫描为线性,每次二分判定至多检查全部原木。
  • 空间复杂度:$O(1)$,只保存搜索边界、已知答案和段数。

关键点总结

[!green]

  • 按根向下取整,才能体现零头无法拼接的限制。
  • 总段数随候选长度单调不增,查找的是最后一个可行长度。
  • 中点记录后移出候选区间,因此循环结束返回 ans,而不是直接返回某个搜索边界。
  • 只要求段数足够,判定无需精确算完所有可切段数。

易错点总结

[!yellow]

  • 下界取零会产生除零,零只作为无解返回值,不参与判定。
  • 将所有原木总长相加后再除,会错误地拼接各根零头。
  • 必须判断段数大于或等于 k,不能拒绝恰好够用的长度。
  • 可行中点要继续向右查找,否则只能得到某个可行长度,不能保证最大。
  • 搜索结束时左端通常已越过最后可行值,不能把它当作答案返回。

相似题目

题目 难度 关联与区别
1891. 割绳子 中等 同样把每根材料独立切成等长正整数段,要求总段数足够,并最大化可行段长。
875. 爱吃香蕉的珂珂 中等 都可二分答案,但本题段数用向下取整且随段长增大而减少,吃香蕉用向上取整计算时间。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2024/16524784
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!