《剑指offer》12、剪绳子(动态规划与贪心)

博客介绍了《剑指offer》中的剪绳子问题,探讨了如何通过动态规划和贪心算法找到使得每段绳子长度乘积最大的剪法。动态规划解法通过状态转移方程实现,时间复杂度为O(n^2)。贪心算法则基于无后效性原则,优先裁剪长度为3的绳子段。两种方法在本问题中得到相同结果,贪心算法效率更高。
摘要由CSDN通过智能技术生成

剪绳子

offer12给出的是一个数学类的问题,现有一段长度为n>1的绳子,要将其剪成m>1段,m,n均为整数,问如何剪才能使得剪断的每一段绳子的长度乘积最大。
比方说,有一段长度为5的绳子,那么有如下的剪法:
[4,1],[3,1,1],[3,2],[2,2,1],[2,1,1,1],[1,1,1,1,1]
其中[3,2]有最大的乘积6.

动态规划

关于动态规划,展开讲的话就没完没了了。我给出了两个知乎上的链接和一个cnblog上的介绍,我觉得这三个地儿讲得足够齐全了。
当然,我们举一个非常简单的例子来说明动态规划的核心:我比较认可的是第一个链接中的一个说法,动态规划就像魔改的数列题,核心是在于写出状态转移方程,也就是将下一个状态写成和上一个状态有关的例子。我们正是拿这个数列说事——斐波那契数列。
我们知道斐波那契数列可以写成通项
F n = ( 1 + 5 2 ) n − ( 1 − 5 2 ) n 5 F_{n}=\frac{\left(\frac{1+\sqrt{5}}{2}\right)^{n}-\left(\frac{1-\sqrt{5}}{2}\right)^{n}}{\sqrt{5}} Fn=5 (21+5 )n(215 )n
但没多少人会这么写,更一般的形式是:
{ F ( 1 ) = F ( 2 ) = 1 F ( n ) = F ( n − 1 ) + F ( n − 2 ) n ≥ 3 \left\{\begin{array}{c} F(1)=F(2)=1 \\ F(n)=F(n-1)+F(n-2) \quad n \geq 3 \end{array}\right. {F(1)=F(2)=1F(n)=F(n1)+F(n2)n3
这个公式实际上就是一种状态转移方程。我们将 F ( n ) F(n) F(n)拆分成了 F ( n − 1 ) F(n-1) F(n1) F ( n − 2 ) F(n-2) F(n2)的组合,这样一来就不用重复地计算 F ( n − 1 ) F(n-1) F(n1) F ( n − 2 ) F(n-2) F(n2),节省了大量的时间。
在剪绳子的问题上,我们已经有 F ( 2 ) = 1 , F ( 3 ) = 2 F(2)=1,F(3)=2 F(2)=1,F(3)=2,最关键的一步就是写出 F ( i ) F(i) F(i)的状态转移方程: F ( i ) = max ⁡ [ F ( j ) ∗ F ( i − j ) ] F(i)=\max [F(j) * F(i-j)] F(i)=max[F(j)F(ij)],意思是,对于一个新的长度,我们可以遍历原来的所有的 F ( i − 1 ) F(i-1) F(i1),找到 max ⁡ [ F ( j ) ∗ F ( i − j ) ] \max [F(j) * F(i-j)] max[F(j)F(ij)]。如此一来,只需要一次正向和一次反向的遍历,便以O(n^2)的时间复杂度解决该问题。
代码如下:

# offer12-solution 1
# DP
def MaxProductAfterCut(n):
    products = [0, 1, 2, 3]
    if n < 2:
        print(products[0])
        return
    elif n == 2:
        print(products[1])
        return
    elif n == 3:
        print(products[2])
        return
    for i in range(4, n + 1):
        max = 0
        for j in range(1, i // 2 + 1):
            product = products[j] * products[i - j]
            if product > max:
                max = product
        products.append(max)
        # print(products)
    print(products[n])
    return

贪心算法

关于贪心算法的参考文献也见参考目录,简单地说,贪心算法是指在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,它所做出的仅仅是在某种意义上的局部最优解。
这里就会存在疑惑:贪心算法不是对所有问题都能得到整体最优解,那么有没有办法证明贪心算法对哪些问题是奏效的呢?对此其实并没有一个定论,但是如果学过随机过程的话,其实可以比较简单的证明:贪心策略必须具备无后效性。也就是说,所有可以在无后效性的前提下分解问题的策略,都可以考虑贪心算法。而剪绳子则是一个无后效性问题——剪完一刀以后剪掉的部分就成了“过去式”,不会再影响以后的裁剪。
在这个前提下,我们思考剪绳子的局部最优策略:
在刚才的动态规划中,我们已经知道:
{ F ( 1 ) = 0 F ( 2 ) = 1 ∗ 1 = 1 F ( 3 ) = 2 ∗ 1 = 2 F ( 4 ) = 2 ∗ 2 = 4 F ( 5 ) = 3 ∗ 2 = 6 \left\{\begin{array}{c} F(1)=0 \\ F(2)=1 * 1=1 \\ F(3)=2 * 1=2 \\ F(4)=2 * 2=4 \\ F(5)=3 * 2=6 \end{array}\right. F(1)=0F(2)=11=1F(3)=21=2F(4)=22=4F(5)=32=6
自然我们不能再往后一个一个罗列了,但我们依然可以罗列一个事实:
3 ( n − 3 ) > 2 ( n − 2 )  when  n > 5 3(n-3)>2(n-2) \text { when } n>5 3(n3)>2(n2) when n>5
这意味着当绳子长度大于5的时候,截取出一段3要优于截取出一段2。
但这时候又存在一个问题了,有人可能会说,那下面这个也是事实:
p ( n − p ) > ( p − 1 ) ( n − p + 1 )  when  n > 2 p − 1 ( p ∈ N ∗ ) p(n-p)>(p-1)(n-p+1) \text { when } n>2 p-1\left(p \in N^{*}\right) p(np)>(p1)(np+1) when n>2p1(pN)
诚然,如果考虑上式,我们可以得出结论:当绳子长度大于29的时候,截取一段15比截取一段14要更优秀。但是显然我们不可能对着一段长度为15的绳子无动于衷,因此当我们对 n = 15 n=15 n=15向下考虑的时候,最终仍然会回到——剪一段3还是剪一段2的问题上。那就回到了第一式了。
因此我们可以依循贪心策略得出结论:
首先先尽量裁剪长度为3的绳子段,直到剩余的绳子长度小于5,然后如果是4,就裁成两个2(不裁也可以),如果是3和2就不动了。根据这个思路,代码如下:

# offer12-solution 2
# Greedy Algorithm
def MaxProductAfterCut2(n):
    if n < 2:
        print(0)
        return
    if n == 2:
        print(1)
        return
    if n == 3:
        print(2)
        return
    pow3 = n // 3
    if n - pow3 * 3 == 1:
        pow3 -= 1 # 如果剩余长度是4,那就要回退一步,不能裁成3+1
    pow2 = (n - pow3 * 3) // 2 # 剩余4→2+2,剩余2,不动
    print((3 ** pow3) * (2 ** pow2))
    return

小结

两个解法的结果是一样的。贪心明显具有更低的时间复杂度,但是DP的适用范围会更广泛一些,贪心策略的严格证明也是比较困难的,实际应用的时候要选择适合的策略和思路。

参考

如何理解动态规划
什么是动态对话
【算法复习】动态规划
《剑指offer》09&13&14、数学类问题:斐波那契数列、二进制数中1的个数、数值的整数次方
五大常用算法之一:贪心算法
漫画:五分钟学会贪心算法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值