剑指Offer 14.剪绳子(循环求余法,快速幂求余)

剑指Offer 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。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

示例:

Input: 2
Output: 1 即2 = 1+1,1*1=2
Input: 10
Output: 36 即10 = 3 + 3 + 4, 3 × 3 × 4 = 36

解题思路:
经过简单分析,我本题的主要思路是动态规划,动态规划能够解决的问题是:原问题可以分解为与原问题类似或相同的小问题,而剪绳子恰恰就是这样的一类问题。假如我们先把绳子剪为两段, x x x y y y 此时 x + y = n x+y=n x+y=n,乘积则为 x y xy xy,那么xy什么时候会是最大呢,只有当 x x x y y y各自都是最大值时,才会使得乘积最大。这里面就隐含了一个递归的过程, x x x y y y继续各自剪短,直到求得一个最大值,但是在我们不断剪绳子的过程中,有的绳子的长度会一样,而他们剪短的最优方式也是相同的,这时候如果我们将最优的剪绳方式记录下来,就可以避免重复计算浪费时间。

我们可以使用一个数组,将每段绳子的最大乘积记录下来,这样比它们的长的绳子当剪成一样的大小时,就可以直接相乘,避免重复计算。而找到每段绳子的最优剪法的方式是一样的,就是循环,从剪成1与n-1开始,直到剪到 n / 2 + 1 n/2+1 n/2+1,这时就会开始重复之前的剪法。

算法代码:

    public int cuttingRope(int n) {
        int[] record = new int[n+1];
        record[1] = 1;            
        for(int count = 2; count < n+1; count++){
            for(int i = 1; i < count/2 + 1; i++){
                record[count] = Math.max(record[count], Math.max((count-i)*i, record[count-i]*Math.max(record[i], i)));
            }
        }
        return record[n];
    }

但是对于这题的最终答案并不能这样,因为它存在一个大数求余的过程,而这个大数甚至超过了long的范围,只能使用BigInteger来进行存储,这时效率是极低的。

而leetcode上还有另一种解法,大神们发现一个段绳子,当剪成长度为3的绳子尽可能多的时候,乘积会最大,但是会有两个例外,当绳子为4时,这时候应剪成 2 ∗ 2 = 4 2*2=4 22=4;当绳子为5时,应剪成 2 ∗ 3 2*3 23,具体证明请见文末参考文章。但是由于K神的两种求余的思路比较难懂,因此我借此记录下我理解的过程,以便日后复习。

循环求余法

public int cuttingRope(int n) {
    if(n<4)
        return n-1;
    long rem = 1;
    while(n>4){
        rem *= 3;
        rem %= 1000000007;
        n -= 3;
    }
    return (int)(rem * n % 1000000007);
}

理解这个代码的前提是求余的性质,也就是
( x y ) % p = [ ( x % p ) ( y % p ) ] % p (xy)\%p = [(x\%p)(y\%p)]\%p (xy)%p=[(x%p)(y%p)]%p
举个简单的例子 90 % 10 = 0 90\%10 = 0 90%10=0 ==> ( 2 ∗ 3 ∗ 15 ) % 10 (2*3*15)\%10 (2315)%10 ==> [ ( 2 ∗ 3 ) % 10 ∗ ( 15 % 10 ) ] % 10 = 0 [(2*3)\%10 * (15\%10)]\%10 = 0 [(23)%10(15%10)]%10=0,可见当一个数求余时,可以将其拆解成多个数分别求余后相乘再求余。也就是在这题中我们希望3的数量尽可能多,这时候 n = 3 a + b n = 3^a + b n=3a+b,也就是将 3 a 3^a 3a拆解为多个不会超出long范围的数分别求余再相乘。

很多人包括我一开始都不懂循环的条件n>4是怎么来的,经过思考,发现 n = 3 a + b n = 3^a + b n=3a+b,发现恰好符合求 b b b的过程,因为上面我们说求乘积最大时有两个例外,当绳子为4时,这时候应剪成 2 ∗ 2 = 4 2*2=4 22=4;当绳子为5时,应剪成 2 ∗ 3 2*3 23。由于都要取出1个3来与余数1或2相乘凑成4或6,因此n>4就可以少乘一个3,而对应于最后一次循环 n = 7 n=7 n=7这时候循环还要再执行一次,也就是 n = 7 − 3 = 4 n=7-3=4 n=73=4,跳出循环。而绳子长为 n = 5 n=5 n=5时, n = 5 − 3 = 2 n=5-3=2 n=53=2。余数正好就是最后一次循环得到的n值,因此在返回值时只需要rem*n就能满足以上两个特例。

有关int和long型数据范围:

(1) int型一共32位,有一位作为符号位,其数据范围是-2^31 ~ 2^31,
即-2147483648 ~ 2147483647;
近似范围可以记为-2000000000 ~ 2000000000 即 - 2 × 10^9 ~ 2 × 10^9
本题中给的模数为1 × 10^9 + 7(1000000007),若再乘以3,就超过了int型的范围,所以要使用long存储结果才不会溢出

(2) long型一共64位,对应int型的方式,long型数据范围可以简单记为:
-8 × 10^18 ~ 8 × 10^18
本题的1000000007平方小于2 × 10^18,所以用long存储模数的平方也是没有问题的
一个数在求模时只有大于模数才进行取模,因此当一个数接近模数时不会取模,极限值就是模数的平方-1,这是取模时的最大值,也就是模数-1

快速幂求余

对于该解法,K神给的思路较为清楚,主要需要结合表理解。

x a ⊙ p = { ( x 2 ⊙ p ) a / / 2 ⊙ p , a  为偶数  [ ( x ⊙ p ) ( x a − 1 ⊙ p ) ] ⊙ p = [ x ( x 2 ⊙ p ) a / / 2 ] ⊙ p , a  为奇数  x^{a} \odot p=\left\{\begin{array}{ll} \left(x^{2} \odot p\right)^{a / / 2} \odot p & , a \text { 为偶数 } \\ {\left[(x \odot p)\left(x^{a-1} \odot p\right)\right] \odot p=\left[x\left(x^{2} \odot p\right)^{a / / 2}\right] \odot p} & , a \text { 为奇数 } \end{array}\right. xap={(x2p)a//2p[(xp)(xa1p)]p=[x(x2p)a//2]p,a 为偶数 ,a 为奇数 

20210702165943

public int cuttingRope(int n) {
    if(n<4)
        return n-1;
    long rem = 1, x = 3;
    int a = n%3; // 求最后余数来决定凑成什么
    for(int b = n/3 - 1; b > 0; b/=2){
        if(b%2==1)
            rem = rem * x % 1000000007;
        x = x*x%1000000007;
    }
    if(a==0)
        return (int)(rem * 3 %1000000007);
    if(a==2)
        return (int)(rem * 6 %1000000007);
    return (int)(rem * 4 %1000000007);
}

本解法的主要思路是将rem即余数不断变大 ,而在循环中 b = n / 3 − 1 b = n/3 - 1 b=n/31是由于需要将一个3提出来,与1或者2凑成乘积4或6。

如有错误,欢迎大家指出,大家一起进步

参考文章:

  1. 【笛子】分析使用动态规划取模为什么就不行?
  2. 面试题14- II. 剪绳子 II(数学推导 / 贪心思想 + 快速幂求余,清晰图解)
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值