剑指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 2∗2=4;当绳子为5时,应剪成 2 ∗ 3 2*3 2∗3,具体证明请见文末参考文章。但是由于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
(2∗3∗15)%10 ==>
[
(
2
∗
3
)
%
10
∗
(
15
%
10
)
]
%
10
=
0
[(2*3)\%10 * (15\%10)]\%10 = 0
[(2∗3)%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 2∗2=4;当绳子为5时,应剪成 2 ∗ 3 2*3 2∗3。由于都要取出1个3来与余数1或2相乘凑成4或6,因此n>4就可以少乘一个3,而对应于最后一次循环 n = 7 n=7 n=7这时候循环还要再执行一次,也就是 n = 7 − 3 = 4 n=7-3=4 n=7−3=4,跳出循环。而绳子长为 n = 5 n=5 n=5时, n = 5 − 3 = 2 n=5-3=2 n=5−3=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. xa⊙p={(x2⊙p)a//2⊙p[(x⊙p)(xa−1⊙p)]⊙p=[x(x2⊙p)a//2]⊙p,a 为偶数 ,a 为奇数
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/3−1是由于需要将一个3提出来,与1或者2凑成乘积4或6。
如有错误,欢迎大家指出,大家一起进步
参考文章: