最近刷leetcode刷到一道dp题,如下:
剪绳子—1
给你一根长度为 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。
示例 1:
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1
示例 2:输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36提示:
2 <= n <= 58
这题很简单,我使用dp做出来了,代码通过,代码如下:
public int cuttingRope(int n) {
// dp[i]表示长度为i的绳子切分后每段的最大乘积
int[] dp = new int[n+1];
dp[2] = 1;
for (int i=3;i<=n;++i){
// i-j >= 2
for (int j=1;j<=i-2;++j){
// 这里因为m>1,所以dp[2] = 1而不是2,dp[3]不能是dp[2] * 1,这样答案是1,错误。
dp[i] = Math.max(Math.max(dp[i-j], i-j) * j, dp[i]);
}
}
return dp[n];
}
然后第二天,我刷到了一道同样的题,只不过n的范围变了,同时增加了结果取余的提示,增加的内容如下:
剪绳子-2
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
提示:
2 <= n <= 1000
我本想,把dp改成long类型数组,然后在每次计算dp[i]的时候对dp[i]进行取模操作就可以了,结果不正确,错误代码如下:
public int cuttingRope(int n) {
// dp[i]表示长度为i的绳子切分后每段的最大乘积
long[] dp = new long[n+1];
dp[2] = 1;
for (int i=3;i<=n;++i){
// i-j >= 2
for (int j=1;j<=i-2;++j){
// 这里因为m>1,所以dp[2] = 1而不是2,dp[3]不能是dp[2] * 1,这样答案是1,错误。
dp[i] = Math.max(Math.max(dp[i-j], i-j) * j, dp[i]) % 1000000007;
}
}
return (int)dp[n];
}
当n取到60时就发生了错误,看来官方自己也是通过dp算法测试过。很奇怪,以往取模的运算也都是对结果取模,再不济对运算时中的每个自运算式取模,结果都是正确的,这次却怎么也不对。其实这里错误跟取模没关系,是因为取模导致了dp的运算出现了问题。dp是通过最优子问题来计算出最终结果的,而取模之后就导致计算最优子问题出现了问题,计算出来的dp[i-j]*j表面上可能是最大的,但是dp[i-j]也是经过取模运算的,所以可能会出现:
dp[p] > dp[q],但是dp[p] % 1000000007 < dp[q] % 1000000007,从而这会导致dp[i]不是由前面的最优子问题推出来的。
因此,使用dp时,前面的结果不能取余,要保留完整的值来进行比较,通过比较确定最优的子问题结果。
后面我又改了一下代码,想把每次计算的结果保存下来,用dp数组保存商和余数,如下:
class Solution {
public int cuttingRope(int n) {
// dp[i][0]*1000000007+dp[i][1]就是长度为n的绳子剪成m段能得到的最大乘积
// dp[i][0]表示长度为i的绳子的最大乘积除以1000000007的商,dp[i][1]表示长度为i的绳子的最大乘积模上1000000007的余数
long[][] dp = new long[n+1][2];
int modNum = 1000000007;
dp[2][0] = 0;
dp[2][1] = 1;
for (int i=3;i<=n;++i){
for (int j=1;j<=i-2;++j){
long[] k = dp[i-j];
long a,b;
if (k[0] > 0 || k[1] > i-j){
a = k[0] * j + (k[1]*j)/modNum;
b = (k[1]*j)%modNum;
}else{
a = 0;
b = (i-j)*j;
}
if (a > dp[i][0] || (a == dp[i][0] && b > dp[i][1])){
dp[i][0] = a;
dp[i][1] = b;
}
}
}
return (int)dp[n][1];
}
}
这次,能正确计算到n=130左右,但是大了还是会出现问题,问题就是计算的结果太大了,dp[i][0]存不下商,可想而知数是由多大,毕竟如果n=1000,一段长度为2,那乘积就是2^500。这里可以通过用字符串或大数来存储乘积模上1000000007的商,这样就不会溢出了。
官方解答如下,尽可能的剪出长度为3的小段,就能获得最大值,其中2,3,4是例外,不能剪出长度为3的段,所以代码如下:
public int cuttingRope(int n) {
int a = n/3, b = n%3, modNum = 1000000007;
long res = 1;
if (n == 2) return 1;
if (n == 3) return 2;
if (n == 4) return 4;
// 此处只乘x-1个3
for (int i=1;i<a;++i){
res = (res*3)%modNum;
}
// 处理最后的2,3,4问题
if (b == 0) res = res*3%modNum;
if (b == 1) res = res*4%modNum;
if (b == 2) res = res*6%modNum;
return (int)res;
}