问题引入
给你一根长度为 n 的绳子,请把绳子剪成整数长度的若干段,然后把各段长度相乘,能得到的最大乘积是多少?(注:长度n至少为2,你至少要剪一次)
举个例子:
绳子长度为10,那么我们把它剪成 3+3+4 的多段,可以得到最大值 3×3×4=36 。
初步结论
2 = 1 + 1,1 × 1 = 1 | 2不应该被切 |
3 = 1 + 2,1 × 2 = 2 | 3不应该被切 |
4 = 2 + 2,2 × 2 = 4 | 4切不切都一样 |
5,6,7,8,9… | 要切(且任何数字都可以被切分为2和3的组合) |
解法一:动态规划
根据上面的初步结论,不难想到动态规划的写法:
初始化:dp[1]=1(无意义),dp[2]=1,dp[3]=2
状态转移:dp[i] = Math.max(dp[i - 2] * 2, dp[i - 3] * 3)
代码:
class Solution {
public int cuttingRope(int n) {
if (n == 2) {
return 1;
}
if (n == 3) {
return 2;
}
int[] dp = new int[n + 1];
dp[1] = 1;
dp[2] = 2;
dp[3] = 3;
for (int i = 4; i <= n; i++) {
dp[i] = Math.max(dp[i - 2] * 2, dp[i - 3] * 3);
}
return dp[n];
}
}
解法二:贪心
不妨找找规律:
2=1+1,res=1
3=1+2,res=2
4=2+2,res=4
5=3+2,res=6
6=3+3,res=9
7=3+4,res=12
8=3+3+2,res=18
9=3+3+3,res=27
10=3+3+4,res=36
11=3+3+3+2,res=54
大胆猜测:
在尽可能切分出3的情况下,对4予以保留。
我们可以这样做,用3取余——如果余数为0,那正好全切为3;如果余数为1,那么少切一个3,组成一个4;如果余数为2,那就前面全切为3,最后切一个2。
小心求证:
Krahets大神的提供的数学推导:戳这里
代码:
class Solution {
public int cuttingRope(int n) {
if (n == 2) {
return 1;
}
if (n == 3) {
return 2;
}
int a = n / 3;
int b = n % 3;
if (b == 0) {
return (int) Math.pow(3, a);
} else if (b == 1) {
return (int) (Math.pow(3, a - 1) * 4);
} else if (b == 2) {
return (int) (Math.pow(3, a) * 2);
}
return 0;
}
}
另一种写法:
class Solution {
public int cuttingRope(int n) {
if (n == 2) {
return 1;
}
if (n == 3) {
return 2;
}
int res = 1;
while (n > 4) {
n -= 3;
res *= 3;
}
return res * n;
}
}
扩展题目
Leetcode中对原题目进行了修改,将n的取值设置到近150。这样的话,最终结果远超long类型。为此,题目要求最终结果对 1000000007 取余。
这导致了,动态规划法无法使用。因为Math.max运算变得没有意义——比较取模后的数字,并不能反映取模前两个数字的大小。
好在,贪心法还可以使用:
写法一:Math.pow不能用了?那就自己写一个带取模的快速幂!
class Solution {
public int cuttingRope(int n) {
if (n == 2) {
return 1;
}
if (n == 3) {
return 2;
}
int a = n / 3;
int b = n % 3;
if (b == 0) {
return (int) (quickPow(3, a) % 1000000007);
} else if (b == 1) {
return (int) (quickPow(3, a - 1) * 4 % 1000000007);
} else if (b == 2) {
return (int) (quickPow(3, a) * 2 % 1000000007);
}
return 0;
}
// 快速幂(非递归实现,带取模)
private long quickPow(long a, int n) {
long res = 1;
while (n > 0) {
if ((n & 1) == 1) {
res *= a;
res %= 1000000007;
}
a *= a;
a %= 1000000007;
n >>= 1;
}
return res;
}
}
写法二:似乎更加简单,每步取模即可。
class Solution {
public int cuttingRope(int n) {
if (n == 2) {
return 1;
}
if (n == 3) {
return 2;
}
long res = 1;
while (n > 4) {
n -= 3;
res *= 3;
res %= 1000000007;
}
return (int) (res * n % 1000000007);
}
}
理解本质
最后,再通俗的说一下贪心法的数学推导过程。
思考一下,一个长方形的周长一定,如何得到最大面积?很简单,使其成为一个正方形。
同理,我们把绳子剪成若干个长度相同的段,就能使得所有段的乘积最大。
问题是,这个长度取多少合适呢?
列方程求导即可。答案是长度为3(驻点实际上是e)。
所以,我们尽量把绳子剪成若干个长度为3的段,如果不凑整是3的倍数,那么就退而求其次,最后剩下2或4。但是,绝不能出现1。这也是如果余1,也要凑4的原因。
E N D END END