动态规划与取余问题——剪绳子

最近刷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;
    }

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值