剑指Offer14- I&&II -- dp

剪绳子I

1. 题目描述

dp



2. 思路

这题的思路还是比较简单的,就是划分子区间,一个长度为 \(k\) 的绳子,我们可以将其划分为 \(i\) + \(j\),(\(i>=0,j>=0\))。
我们用 \(f[k]\) 表示长度为 \(k\) 的绳子划分长度之后的最大乘积,则:
\(f[i] = max(f[i], f[i] * f[j])\),NO NO NO,不是这样的!如果你只是这么简单的想,那就大错特错了!
虽然说,我们将 \(k\) 划分为了 \(i\) + \(j\),但是 \(i\)\(j\) 有必要划分吗?就例如 \(2\),它不划分时的长度乘积为 \(2\),划分之后的长度乘积为 \(1*1=1\),可以看出,它不划分时的乘积更大!当然,只有 \(2\) 作为被划分的子区间时才可以不继续划分,如果 \(k=2\),那么我们就必须划分了。
因此,我们需要判断子区间,是否继续划分,即:

f[k] = max(f[k], f[i] * f[j]);  // 两个都划分
f[k] = max(f[k], i * f[j]);    // 划分 j
f[k] = max(f[k], f[i] * j);    // 划分 i
f[k] = max(f[k], i * j);    // 都不划分
// 当然,上买的形式太复杂了。我们可以简化为:
f[k] = max(f[k], max(i, f[i]) * max(j, fp[j]));

综上,我们其实就讨论了一个容易犯问题,给定原始长度 \(k\),我们是必须要划分的,但是划分之后的长度\(i\)\(j\),可以划分,也可以不划分!
可能你会问?为什么是划分为两段,而不是三段,四段。。。?
划分为二段,三段不行!因为两段肯定包含三段,四段的情况,而三段不可能包含两段的情况,除非你划分一个 \(0\),但那样乘积也为 \(0\)了,同理,四段不可能包含两段和三段的情况。



3. 代码

class Solution {
public:
    int cuttingRope(int n) {
        int f[60];
        // f[i][j] 表示长度为 i 的绳子剪成 j 段的最大乘机 
        memset(f, 0, sizeof f);
        for(int i = 2; i <= n; i ++ ) {
            for(int j = 0; j <= i; j ++ ) {
                // [j] [i-j]
                int a = max(i - j, f[i - j]);
                int b = max(j, f[j]);
                f[i] = max(a * b, f[i]);
            }
        }
        return f[n];
    }
};

剪绳子2

1. 题目描述

相较于上一题,本体数据范围由58上升到2000,这个数据范围再用上一题的方法肯定会溢出了!因此本题需要对答案取模,但是取模就意味着大小关系可能有问题!原本我是比你大的,但取模之后我比你小了!还会做吗?



2. 思路

前面已经分析过了,\(dp\) 是用不了了!那怎么办呢?
我们可以尝试分析一下规律,挖掘出来一些性质。我们可以通过 \(dp\) 求出来长度为 \(1~20\) (当然,样例够用就行)的绳子划分之后的最大长度,看看有没有什么规律。

dp[2] = 1 * 1
dp[3] = 1 * 2
------------
dp[4] = 2 * 2
dp[5] = 2 * 3
dp[6] = 3 * 3
dp[7] = 2 * 2 * 3
dp[8] = 2 *3 * 3
dp[9] = 3 * 3 * 3
dp[10] = 3 * 3 * 2 * 2
dp[11] = 3 * 3 * 3 * 2
dp[12] = 3 * 3 * 3 * 3
dp[13] = 3 * 3 * 3 * 2 * 2

我们将 \(k=2/3\) 的情况单独拿出来,是因为他们包含 \(1\),而其余都不包含 \(1\),观察 \(dp[4]~dp[13]\),可以发现,他们划分之后的长度不是 \(2\) 就是 \(3\),这就是规律!并且,\(3\) 的优先级比 \(2\) 要大。
因此,我们便得到了以下三个规律(\(k>=4\)

  1. 划分因子不是 \(2\) 就是 \(3\),并且 \(2\) 的个数要么为 \(1\),要么为 \(2\),要么为 \(o\)
  2. \(3\) 优先于 \(2\)
  3. \(k=4\) 时,划分为 \(2+2\),而不是 \(3+1\)
    只要找到了这些规律,就可以贪心求解了!
    当然,这种贪心做法对于上一题也是适用的。


3. 代码

贪心 ,O(N)

class Solution {
public:
    int cuttingRope(int n) {
        const int mod = 1e9 + 7;
        if(n == 2)  return 1;
        if(n == 3)  return 2;
        if(n == 4)  return 4;
        int res = 1;
        while(n > 4) {
            n -= 3;
            res = (long long)res * 3 % mod;
        }
        // n == 2, 3, 4
        if(n)   res = (long long)res * n % mod;
        return res;
    }
};

贪心+快速幂 O(logN)

class Solution {
public:
    int ksm(int a, int b, int mod) {
        int res = 1;
        while(b) {
            if(b & 1)   res = (long long)res * a % mod;
            b >>= 1;
            a = (long long)a * a % mod;
        }
        return res % mod;
    }
    int cuttingRope(int n) {
        if(n == 2)  return 1;
        if(n == 3)  return 2;
        if(n == 4)  return 4;
        const int mod = 1e9 + 7;
        int res = 1;
        int cnt = n / 3;    // 可以划分为多少个3
        if(n % 3 == 1) {    // 特盘下,等剩余一个1时,划分为2+2而不是1+3
            cnt -- ;
            res *= 4;
        }
        else if(n % 3 == 2) {
            res *= 2;
        }
        return (long long)res * ksm(3, cnt, mod) % mod;
    }
};


4. 参考

ref
严谨的数学证明

posted @ 2023-03-05 10:30  光風霽月  阅读(13)  评论(0编辑  收藏  举报