35.动态规划(3) | 整数拆分(h)、不同的二叉搜索树(h)

        今天的2道题都没有自己做出来,感觉比较难想到解法,但解法本身并不复杂。这2道题的重点都在状态转移方程。它们不再依赖于前一两个或者前一行的某个数字,而是依赖于之前所有的dp值。每求一个dp数值时,都需要进行内层循环,或是在内层循环中取一个最大值,或是把循环中的某个结果值都相加。感觉重点是要把问题分成若干情况来讨论,并且让每种情况能对应到规模更小,类型相同的子问题。


        第1题(LeetCode 343. 整数拆分)属于想了半天还是不会,看了题解又发现并不是很难的类型。首先定义方面,dp[i]表示数字i被拆分的最大成乘积。对于数字i,它总是会被拆分成2个或2个以上的数字。对于2个数字的情况,只需要让j从1到(i - 1)遍历,然后不断计算j * (i - j),取其中的最大值即可。而对于两个以上数字的情况,仍然需要让j从1到(i - 1)遍历,只不过需要计算的是j * dp[j]。由dp数组的定义,数字i被拆分后众多数字中的一个一定在[1, i - 1]之间,那么其余数字的最大乘积就是dp[j]。所以这道题的状态转移方程是dp[i] = max(j * (i - j), j * dp[j]),其中j需要从1遍历到(i - 1)。

        初始化方面,数字0和1并没有能被拆分的定义,理应从数字2开始定义。但在计算dp[i]的内层循环,仍是要从数字1开始遍历(虽然当j取1时,只用计算j * (i - j))。所以为了内层循环的写法统一,可以将dp[1]初始化为1或者更小的数字。然后遍历的顺序就是从小到大,对于每个数字i,又在内层循环中从1~(i-1)遍历。

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

        我自己的代码中用了一个变量来存储dp[i]的中间结果,但看了题解,发现可以直接用dp[i]来代替这个多余的变量。并且将dp数组初始化为0(dp[1]除外)的话,也就不用在内层循环之前对dp[i]初始化了。

        题解还有另一种数论相关的贪心解法,感觉如果没有做过的话一时半会一定推导不出来的。这种解法就是将数字尽可能的拆分为3,如果最后一个数字是4的话就保留4,否则继续拆分为3和另外一个小于3的数。具体推导很复杂,所以也没学习。

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

        二刷:刚开始忘记方法,最后想起来,以及忘记dp[i]除了要跟dp[j] * (i - j)比较取较大值之外,还要跟j * (i - j)比较。


        第2题(LeetCode 96. 不同的二叉搜索树)也是自己看了半天还是找不到关系,看了题解。首先定义方面,还是将dp数组定义为题目的计算目标,即dp[i]表示由1~i,共i个不同数字作为节点的不同二叉搜索树数量。

        状态转移方程是这道题的关键。每棵二叉搜索树有1个根节点,其余的节点都要么在左子树中,要么在右子树中。对于由1~i的i个数字组成的二叉搜索树,可以按照左右子树节点的数量来分为若干情况。如果左子数有j个节点,那么右子树就有(i - 1 - j)个节点,j可以从1取到(i - 1),所以总共有(i - 1)种情况。这其中j和(i - 1 - j)都一定是小于i的,所以其左子树的类型就有dp[j]种,右子树的类型就有dp[i - 1 - j]种,两者相乘就得到了“左子树有j个节点情况下,整个树可能的结构数”。在按照这种方式将j从0取到(i - 1),将每个j对应的树可能的结构数相加,就得到了dp[i]。所以状态转移方程就是dp[i] += dp[j] * dp[i - 1 - j],其中j要从0遍历到(i - 1)为止。

        初始化方面,由于在内部循环时j要从0开始取,所以将dp[0]初始化为1。因为某棵子树为空的排列方式只有一种,所以这样设置也是符合规则的。遍历顺序同上一题一样是从左到右,且需要内层循环。

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

        二刷:刚开始忘记方法,最后想起来。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值