题目描述

✅ 剑指 Offer 14- I. 剪绳子

image-20261001230752538

image-20260928201345416

题意分析

将长度为 n 的绳子剪成至少两段,每段长度都是正整数,求各段长度乘积的最大值。所有段的长度之和必须仍为 n。

至少剪一刀是硬性要求,不能直接用整条绳子的长度作为答案。特别是长度二和三,虽然不剪更大,仍要返回剪开后的最大乘积。本题范围为 2 <= n <= 58,答案使用整数返回,不需要取模。

解法:贪心优先剪出长度 3

核心思路

[!blue]

先分析最优方案里会有哪些长度。对任意一段 $x\ge5$,拆成 $3$ 和 $x-3$ 后,乘积为 $3(x-3)$,与不拆相比增加 $2x-9>0$。因此最优方案不会保留大于等于五的段。

当总长至少为四时,也没必要保留长度一:它与长度二可以合成三以增大乘积;与长度三可以换成两个二;与长度四可以换成二和三;两个一也可以合成二。所以最优方案只需要长度二、三,以及等价于两个二的四。

剩下只需决定二和三的比例。同样总长六,三个二的乘积是八,两个三的乘积是九,所以三个二总能换成两个三而变得更优。最优方案应尽量用三,最多保留两个二。

因此每次从剩余长度中取出三,但不能留下长度一。代码只在 n > 4 时继续取三,最后剩下二、三或四,直接乘入结果。剩四代表拆成两个二,乘积恰好也是四;这并不违反至少剪成两段的要求。

对 n = 2、n = 3,上述“避免一”的调整无法同时满足至少两段,需要单独返回 n - 1。

解题步骤

  1. 若 n <= 3,返回 n - 1,满足必须剪开。
  2. 初始化已经剪出部分的乘积 result = 1。
  3. 只要剩余长度 n > 4,就剪出一段三,将 result 乘三并将 n 减三。
  4. 返回 result * n;最后剩四时按两个二的乘积理解。

代码实现

class Solution {
    public int cuttingRope(int n) {
        if (n <= 3) {
            return n - 1;
        }

        int result = 1;

        // 剩四时保留为二乘二,避免拆成三加一而降低乘积。
        while (n > 4) {
            result *= 3;
            n -= 3;
        }

        return result * n;
    }
}
func cuttingRope(n int) int {
    if n <= 3 {
        return n - 1
    }

    result := 1
    // 剩四时保留为二乘二,避免拆成三加一而降低乘积。
    for n > 4 {
        result *= 3
        n -= 3
    }
    return result * n
}

复杂度分析

  • 时间复杂度:$O(n)$,每次循环将剩余长度减少三。
  • 空间复杂度:$O(1)$,只维护剩余长度和乘积。

关键点总结

[!green]

  • 通过替换证明排除长段和长度一,再比较同总长的二与三。
  • 三优先,但余一需要改成两个二,不能机械地一直剪三。
  • 只在剩余长度大于四时继续,统一处理所有余数情况。

解法:动态规划枚举第一段

核心思路

[!blue]

也可以不预先推导最优段长,而是直接枚举切法。令 dp[length] 表示长度为 length 的绳子至少剪一刀后,能够得到的最大乘积。

枚举第一段长度 first,范围是 1..length-1,剩余部分长度为 length-first。第一段作为完整的一段保留,剩余部分可以不再剪,贡献它本身的长度;也可以继续剪,贡献对应的 dp 值。因此这次选择的最优乘积为 first * max(length-first, dp[length-first])。

任意合法切法都有一个第一段,剩余部分正好属于上述“不剪”或“继续剪”两类。枚举所有第一段并取最大值,就不会遗漏最优方案,也不需要再枚举左段内部切法。

dp[0]、dp[1] 保持零,表示无法剪成两段;从长度二开始递增计算,右段总比当前绳子短,所需状态已经算好。第一刀由 first < length 保证,子段允许不再剪则由转移中的原长度保证。

解题步骤

  1. 创建长度为 n + 1 的全零数组 dp。
  2. 按长度从二到 n 依次计算。
  3. 对每个长度枚举第一段,比较右段不剪与继续剪的乘积贡献。
  4. 用所有选择的最大乘积更新当前状态,最终返回 dp[n]。

代码实现

class Solution {
    public int cuttingRope(int n) {
        int[] dp = new int[n + 1];
        for (int length = 2; length <= n; length++) {
            for (int first = 1; first < length; first++) {
                int rest = length - first;
                dp[length] = Math.max(dp[length], first * Math.max(rest, dp[rest]));
            }
        }
        return dp[n];
    }
}
func cuttingRope(n int) int {
    dp := make([]int, n+1)
    for length := 2; length <= n; length++ {
        for first := 1; first < length; first++ {
            rest := length - first
            bestRest := rest
            if dp[rest] > bestRest {
                bestRest = dp[rest]
            }
            product := first * bestRest
            if product > dp[length] {
                dp[length] = product
            }
        }
    }
    return dp[n]
}

复杂度分析

  • 时间复杂度:$O(n^2)$,每个长度枚举一次全部第一段选择。
  • 空间复杂度:$O(n)$,保存各长度的最优乘积。

关键点总结

[!green]

  • 整条绳子必须剪开,与剪出的子段是否继续剪,是两个不同层次的要求。
  • 第一段枚举保证至少两段,右段取原长度或 DP 值覆盖全部后续切法。
  • 状态按长度递增计算,依赖总是较短的绳子。

易错点总结

[!yellow]

  • 对长度二、三直接返回 n,等价于没有剪,违反至少两段的条件。
  • 贪心循环使用 n >= 4 会把最后的四拆成三与一,降低乘积。
  • 动态规划中的 dp[i] 表示必须剪开的最大乘积,不能让它无条件与未剪的 i 取大。
  • 转移时又不能强迫右段也剪开,需要比较右段原长度与它继续剪开的最优值。
  • 本题没有取模要求,不要沿用剪绳子 II 的取模处理来计算或比较乘积。

相似题目

题目 难度 关联与区别
剑指 Offer 14- II. 剪绳子 II 中等 最大乘积的拆分规律相同,原题范围更大且要取模,不能把取模后的值直接用于大小比较。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2023/74900259
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!