代码随想录第41天

1.整数拆分:

首先先来写出前几个的答案:
0 和1  没意义

2        1*1             1

3        1*1*1         1

          1*2             2

4        1*1*1*1      1

          2*1*1         2

          3*1             3

          2*2             4

5        1*1*1*1*1    1

          1*1*1*2        2

          1*1*3           3

          1*2*2           4

          1*4              4

          2*3              6

然后在看完下面的五部曲之后的问题:

1.

为什么j遍历,只需要遍历到 n/2 就可以,后面就没有必要遍历了,一定不是最大值?

因为首先只有拆分的数接近相乘才是最大,过了n/2后,两个数的差值只会越来越大,所以不会有最大值,而且后面的情况其实在前面已经考虑过了。

比如:n=5

拆成  j   i-j  :1 4;2 3;

其实1*4如果拆分的话已经包括了上面写的5的前面4种情况,2*3如果拆分的话包括第2和4种,如果接下来遍历的话就是3*2 和4*1,3*2本身和2*3一样,如果拆分的话就是包括第3种,4*1本身和1*4一样,还拆不了。所以只要遍历到n/2就可以了。

2.

递推公式:dp[i] = max({dp[i], (i - j) * j, dp[i - j] * j});

那么在取最大值的时候,为什么还要比较dp[i]呢?

for (int i = 3; i <= n ; i++) {
    for (int j = 1; j <= i / 2; j++) {
        dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
    }
}

取遍历里面那个for的最大值,因为不能保证在遍历里面的for循环时最后的一定是最大的

比如n=8的时候拆成3*5是max(3*5,3*dp[5])为18 和4*4是max(4*4,4*dp[4])为16

3.j怎么就不拆分呢?

其实和1的问题差不多,就是他在遍历过程中已经考虑到了:
比如n=5的时候

j  i-j  拆成1 4和2 3,就拿第2个为例:

1*1*3;i-j不拆的情况下

1*1*1*1*1;这两个都是i-j拆的情况下

1*1*2*1;

很明显看出这几种情况都在1*4时都考虑到了,所以不用拆。

动规五部曲,分析如下:

  1. 确定dp数组(dp table)以及下标的含义

dp[i]:分拆数字i,可以得到的最大乘积为dp[i]。

       2.确定递推公式

可以想 dp[i]最大乘积是怎么得到的呢?

其实可以定义一个j从1开始遍历到i/2(具体看下面遍历顺序),然后有两种渠道得到dp[i].

一个是j * (i - j) 直接相乘。

一个是j * dp[i - j],相当于是拆分(i - j)。

那有同学问了,j怎么就不拆分呢?

j是从1开始遍历,拆分j的情况,在遍历j的过程中其实都计算过了。那么从1遍历j,比较(i - j) * j和dp[i - j] * j 取最大的。递推公式:dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));

也可以这么理解,j * (i - j) 是单纯的把整数拆分为两个数相乘,而j * dp[i - j]是拆分成两个以及两个以上的个数相乘。

如果定义dp[i - j] * dp[j] 也是默认将一个数强制拆成4份以及4份以上了。

所以递推公式:dp[i] = max({dp[i], (i - j) * j, dp[i - j] * j});

那么在取最大值的时候,为什么还要比较dp[i]呢?

因为在递推公式推导的过程中,每次计算dp[i],取最大的而已。

    3.dp的初始化

不少同学应该疑惑,dp[0] dp[1]应该初始化多少呢?

有的题解里会给出dp[0] = 1,dp[1] = 1的初始化,但解释比较牵强,主要还是因为这么初始化可以把题目过了。

严格从dp[i]的定义来说,dp[0] dp[1] 就不应该初始化,也就是没有意义的数值。

拆分0和拆分1的最大乘积是多少?

这是无解的。

这里我只初始化dp[2] = 1,从dp[i]的定义来说,拆分数字2,得到的最大乘积是1,这个没有任何异议!

   4.确定遍历顺序

确定遍历顺序,先来看看递归公式:dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));

dp[i] 是依靠 dp[i - j]的状态,所以遍历i一定是从前向后遍历,先有dp[i - j]再有dp[i]。

所以遍历顺序为:

for (int i = 3; i <= n ; i++) {
    for (int j = 1; j <= i / 2; j++) {
        dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
    }
}

注意 枚举j的时候,是从1开始的。从0开始的话,那么让拆分一个数拆个0,求最大乘积就没有意义了。

i是从3开始,这样dp[i - j]就是dp[2]正好可以通过我们初始化的数值求出来。

因为拆分一个数n 使之乘积最大,那么一定是拆分成m个近似相同的子数相乘才是最大的。

例如 6 拆成 3 * 3, 10 拆成 3 * 3 * 4。 100的话 也是拆成m个近似数组的子数 相乘才是最大的。

只不过我们不知道m究竟是多少而已,但可以明确的是m一定大于等于2,既然m大于等于2,也就是 最差也应该是拆成两个相同的 可能是最大值。

那么 j 遍历,只需要遍历到 n/2 就可以,后面就没有必要遍历了,一定不是最大值。

至于 “拆分一个数n 使之乘积最大,那么一定是拆分成m个近似相同的子数相乘才是最大的” 这个我就不去做数学证明了,感兴趣的同学,可以自己证明。

    5.举例推导dp数组

举例当n为10 的时候,dp数组里的数值,如下:

以上动规五部曲分析完毕,C++代码如下:

class Solution {
public:
    int integerBreak(int n) {
        vector<int> dp(n + 1);
        dp[2] = 1;
        for (int i = 3; i <= n ; i++) {
            for (int j = 1; j <= i / 2; j++) {
                dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
            }
        }
        return dp[n];
    }
};

还有一种贪心算法:

依据一个定理:从5开始,每次拆成n个3,最后相乘剩下的(没给出数学证明):

class Solution {
public:
    int integerBreak(int n) {
        if (n == 2) return 1;
        if (n == 3) return 2;
        if (n == 4) return 4;
        int result = 1;
        while (n > 4) {
            result *= 3;
            n -= 3;
        }
        result *= n;
        return result;
    }
};

 注:

关于为什么不拆解j的补充:

说白了如果拆分j就解释不通dp的初始化了

其实这道题目的递推公式并不好想,而且初始化的地方也很有讲究,我在写本题的时候一开始写的代码是这样的:

class Solution {
public:
    int integerBreak(int n) {
        if (n <= 3) return 1 * (n - 1);
        vector<int> dp(n + 1, 0);
        dp[1] = 1;
        dp[2] = 2;
        dp[3] = 3;
        for (int i = 4; i <= n ; i++) {
            for (int j = 1; j <= i / 2; j++) {
                dp[i] = max(dp[i], dp[i - j] * dp[j]);
            }
        }
        return dp[n];
    }
};

这个代码也是可以过的!

在解释递推公式的时候,也可以解释通,dp[i] 就等于 拆解i - j的最大乘积 * 拆解j的最大乘积。 看起来没毛病!

但是在解释初始化的时候,就发现自相矛盾了,dp[1]为什么一定是1呢?根据dp[i]的定义,dp[2]也不应该是2啊。

但如果递归公式是 dp[i] = max(dp[i], dp[i - j] * dp[j]);,就一定要这么初始化。递推公式没毛病,但初始化解释不通!

虽然代码在初始位置有一个判断if (n <= 3) return 1 * (n - 1);,保证n<=3 结果是正确的,但代码后面又要给dp[1]赋值1 和 dp[2] 赋值 2,这其实就是自相矛盾的代码,违背了dp[i]的定义!

我举这个例子,其实就说做题的严谨性,上面这个代码也可以AC,大体上一看好像也没有毛病,递推公式也说得过去,但是仅仅是恰巧过了而已。

2.不同的二叉搜索树

和第一题一样,先来列前几个的答案找规律(其实是找递推公式):

0   1

1    1;2   2:

 3      5:

这时候就有点发现了:

1为头节点时右边是n为2的两种情况,左边为0;

2为头节点时左右两边是n为1的情况;

3为头节点时左边是n为2的两种情况,右边为0;

这时候可以推出:

dp[3],就是 元素1为头结点搜索树的数量 + 元素2为头结点搜索树的数量 + 元素3为头结点搜索树的数量

元素1为头结点搜索树的数量 = 右子树有2个元素的搜索树数量 * 左子树有0个元素的搜索树数量

元素2为头结点搜索树的数量 = 右子树有1个元素的搜索树数量 * 左子树有1个元素的搜索树数量

元素3为头结点搜索树的数量 = 右子树有0个元素的搜索树数量 * 左子树有2个元素的搜索树数量

有2个元素的搜索树数量就是dp[2]。

有1个元素的搜索树数量就是dp[1]。

有0个元素的搜索树数量就是dp[0]。

所以dp[3] = dp[2] * dp[0] + dp[1] * dp[1] + dp[0] * dp[2]

当然其实没这么快,来看看n为4的情况

图就不画了,答案是14

dp[4]

=元素1为头结点搜索树的数量 + 元素2为头结点搜索树的数量 + 元素3为头结点搜索树的数量+元素4为头结点搜索树的数量

=右子树有3个元素的搜索树数量 * 左子树有0个元素的搜索树数量+右子树有2个元素的搜索树数量 * 左子树有1个元素的搜索树数量+右子树有1个元素的搜索树数量 * 左子树有2个元素的搜索树数量+右子树有0个元素的搜索树数量 * 左子树有3个元素的搜索树数量

=dp[3] * dp[0] + dp[2] * dp[1] + dp[1] * dp[2]+dp[0] * dp[3]

=5*1+2*1+1*2+1*5

=14

其实我有一种f分类方法,当然其实是一样的,就是把他分为3类,右类是全在右边,左类全在左边,中类就是分在左右两边

for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= i; j++) {
        dp[i] += dp[j - 1] * dp[i - j];
    }
}

比如n为5

0 4;                                 

1 3;

2 2;

3 1;

4 0;

按照他的意思是:             

是dp[3] * dp[0] + dp[2] * dp[1] + dp[1] * dp[2]+dp[0] * dp[3];

我的意思是:

2*dp[3]+dp[2] * dp[1] + dp[1] * dp[2];

 

当然这样一开始递推公式没出来之前自己就数错了,我这就是提供一个分类思路,看看就行

  1. 确定dp数组(dp table)以及下标的含义

dp[i] : 1到i为节点组成的二叉搜索树的个数为dp[i]

    2.确定递推公式

递推公式:dp[i] += dp[j - 1] * dp[i - j]; ,j-1 为j为头结点左子树节点数量,i-j 为以j为头结点右子树节点数量

     3.dp数组如何初始化

初始化,只需要初始化dp[0]就可以了,推导的基础,都是dp[0]。

那么dp[0]应该是多少呢?

从定义上来讲,空节点也是一棵二叉树,也是一棵二叉搜索树,这是可以说得通的。

从递归公式上来讲,dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量] 中以j为头结点左子树节点数量为0,也需要dp[以j为头结点左子树节点数量] = 1, 否则乘法的结果就都变成0了。

所以初始化dp[0] = 1

     4.确定遍历顺序

首先一定是遍历节点数,从递归公式:dp[i] += dp[j - 1] * dp[i - j]可以看出,节点数为i的状态是依靠 i之前节点数的状态。

那么遍历i里面每一个数作为头结点的状态,用j来遍历。

代码如下:

for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= i; j++) {
        dp[i] += dp[j - 1] * dp[i - j];
    }
}

    5 .举例推导dp数组

n为5时候的dp数组状态如图:

综上分析完毕,C++代码如下:

class Solution {
public:
    int numTrees(int n) {
        vector<int> dp(n + 1);
        dp[0] = 1;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= i; j++) {
                dp[i] += dp[j - 1] * dp[i - j];
            }
        }
        return dp[n];
    }
};

 注:
1.如果写的代码是从3开始遍历的话:

记得前面加上if(n<=2) return n;因为他万一输入1的话,没这句会报错,具体原因嘛就是输入1的时候你创建的vector长度为2,只有0和1,没有dp[2]所以会报错。当然可以用题解的代码这样就没有这种问题,我这样写主要是因为我是从3开始看出这种规律的,但其实从1开始就有这种规律了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值