题 14:剪绳子
给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]...k[m-1] 。请问 k[0]k[1]...*k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。——《剑指 Offer》P96
例如输入 n = 2 时输出应该是 1,这意味着将绳子切分为 2 段,2 段长度都是 1,也就是: 2 = 1 + 1, 1 × 1 = 1。例如输入 n = 10 时输出应该是 36,这意味着将绳子切分为 3 段,3 段的长度分别为 3、3、4,也就是: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
动态规划法
解题思路
要使用动态规划法解这道题,需要将问题分为 n = 2 或 3 和 n > 4 两种情况。当 n = 2 或 3 时,n = 2 时输出应该是 1,这意味着将绳子切分为 2 段,2 段长度都是 1,也就是: 2 = 1 + 1, 1 × 1 = 1。n = 3 时输出应该是 2,这意味着将绳子切分为 2 段,2 段长度分别是 1 和 2,也就是: 3 = 1 + 2, 1 × 2 = 2。
当 n > 4 时,考虑绳子长度为 2 或 3 的情况,例如对长度为 2 的绳子再次分割得到的 2 段绳子的乘积为 1,由于不分割的情况下得到的数值为 2(相当于 1 段长度为 2 的绳子)比不分割带来的效益要小。所以当绳子长度为 2 时,不对绳子继续分割可以得到 1 段长度为 2 的绳子,同理当绳子长度为 3 时,不对绳子继续分割可以得到 1 段长度为 3 的绳子(继续分割的话最大效益为 2)。
此时可以这么考虑问题,例如对长度为 6 的绳子分割有 5 种分割方式:分割为长度为 1 和 5 的 2 段绳子、分割为长度为 2 和 4 的 2 段绳子、分割为长度为 3 和 3 的 2 段绳子、分割为长度为 4 和 2 的 2 段绳子,其中长度为 4 的绳子可以继续分割、分割为长度为 5 和 1 的 2 段绳子,其中长度为 5 的绳子可以继续分割。此时由于分割出的绳子长度为 1 时,对于最终的绳长乘积而言没有任何帮助(显然,因为 1 乘任何数都是其本身),所以解空间可以进行剪枝,排除首尾 2 种情况剩下 3 种分别是:分割为长度为 2 和 4 的 2 段绳子、分割为长度为 3 和 3 的 2 段绳子、分割为长度为 4 和 2 的 2 段绳子,其中长度为 4 的绳子可以继续分割。使用数学语言表示可以表示为:
其中 MaxProduct(2) = 2,MaxProduct(3) = 3,MaxProduct(4) = 4(将绳子切分为 2 段,2 段的长度分别为 2、2,也就是: 4 = 2 + 2, 2 × 2 = 4,计算方式和 n = 5 时一样),可以得出 MaxProduct(5) = 9。将这个规律拓展到一般情况,可以得到动态规划转换方程为:
将这个方程转换为代码就可以解决这个问题,动态规划法的问题分析是从后往前的,而解决问题时需要从前往后依次得到最优解。
题解代码
class Solution:
def cuttingRope(self, n: int) -> int:
#n = 2 或 3
if n == 2:
return 1
if n == 3:
return 2
#n = 4
else:
alist = [0, 1, 2, 3]
for i in range(4, n + 1):
alist.append(0)
for j in range(2, i):
if (i - j) * alist[j] > alist[i]:
alist[i] = (i - j) * alist[j]
return alist[n]
如果不对 n = 2 或 3 进行特殊处理也可以,就需要在循环的每一步判断分割绳子后的效益是否大于不分割的情况。
class Solution:
def cuttingRope(self, n: int) -> int:
alist = [0, 1]
for i in range(2, n + 1):
alist.append(0)
for j in range(1, i):
#判断绳子分割和不分割的情况下哪种效益高
if(alist[j] >= j):
num = (i - j) * alist[j]
else:
num = (i - j) * j
if num > alist[i]:
alist[i] = num
return alist[n]
时空复杂度
动态规划法需要使用二重循环实现,时间复杂度为 O(n^2)。
由于需要一个长度为 n 的数组分别存储 [2,n] 之间的最优解,空间复杂度为 O(n)。
贪心法
解题思路
动态规划法可以得到问题的最优解,但是当需要剪的绳子比较长的时候,由于时间复杂度为 O(n^2) 导致不一定能在短时间内得出解。使用贪心算法往往可以得到一个不错的可行解,同时当局部最优策略能导致产生全局最优解时贪心算法也可以得到最优解。
当按照“当 n > 5 时将绳子分割为长度为 3 的短绳”的贪心策略剪绳子时,得到的各段绳长的乘积最大。简单的证明如下:当 n ≥ 5 时显然 2(n - 2) > n 和 3(n - 3) > n 都成立,且 3(n - 3) ≥ 2(n - 2) 也成立,所以当 n ≥ 5 时将绳子尽可能分成长度为 3 的小段会使得乘积最大。当 n = 4 时可以分为 1 和 3、2 和 2 两种情况,因为 4 = 2 × 2 > 1 × 3,所以 4 留着就可以了。更细致的证明要用到“基本不等式”,可以看的 leetcode 用户逗比克星的题解——无需复杂数学!二元基本不等式分析动态规划、贪心算法。
题解代码
class Solution:
def cuttingRope(self, n: int) -> int:
if n == 2:
return 1
if n == 3:
return 2
else:
num = 1
while n > 4 :
num = num * 3
n = n - 3
return num * n
时空复杂度
贪心算法只需要一层循环就可以实现,时间复杂度为 O(n)。
同时仅需要几个变量存一存中间的结果就可以,空间复杂度为 O(1)。
参考资料
《剑指 Offer(第2版)》,何海涛 著,电子工业出版社
无需复杂数学!二元基本不等式分析动态规划、贪心算法