C++算法 —— 动态规划(12)两道小题

文章目录

  • 1、动规思路简介
  • 2、组合总和Ⅳ
  • 3、卡特兰数


背包问题需要读者先明白动态规划是什么,理解动规的思路,并不能给刚接触动规的人学习。所以最好是看了之前的动规博客,以及背包博客,或者你本人就已经懂得动规了。

1、动规思路简介

动规的思路有五个步骤,且最好画图来理解细节,不要怕麻烦。当你开始画图,仔细阅读题时,学习中的沉浸感就体验到了。

状态表示
状态转移方程
初始化
填表顺序
返回值

动规一般会先创建一个数组,名字为dp,这个数组也叫dp表。通过一些操作,把dp表填满,其中一个值就是答案。dp数组的每一个元素都表明一种状态,我们的第一步就是先确定状态。

状态的确定可能通过题目要求来得知,可能通过经验 + 题目要求来得知,可能在分析过程中,发现的重复子问题来确定状态。还有别的方法来确定状态,但都大同小异,明白了动规,这些思路也会随之产生。状态的确定就是打算让dp[i]表示什么,这是最重要的一步。状态表示通常用某个位置为结尾或者起点来确定。

状态转移方程,就是dp[i]等于什么,状态转移方程就是什么。像斐波那契数列,dp[i] = dp[i - 1] + dp[i - 2]。这是最难的一步。一开始,可能状态表示不正确,但不要紧,大胆制定状态,如果没法推出转移方程,没法得到结果,那这个状态表示就是错误的。所以状态表示和状态转移方程是相辅相成的,可以帮你检查自己的思路。

要确定方程,就从最近的一步来划分问题。

初始化,就是要填表,保证其不越界。像第一段所说,动规就是要填表。比如斐波那契数列,如果要填dp[1],那么我们可能需要dp[0]和dp[-1],这就出现越界了,所以为了防止越界,一开始就固定好前两个值,那么第三个值就是前两个值之和,也不会出现越界。初始化的方式不止这一点,有些问题,假使一个位置是由前面2个位置得到的,我们初始化最一开始两个位置,然后写代码,会发现不够高效,这时候就需要设置一个虚拟节点,一维数组的话就是在数组0位置处左边再填一个位置,整个dp数组的元素个数也+1,让原先的dp[0]变为现在的dp[1],二维数组则是要填一列和一行,设置好这一行一列的所有值,原先数组的第一列第一行就可以通过新填的来初始化,这个初始化方法在下面的题解中慢慢领会。

第二种初始化方法的注意事项就是如何初始化虚拟节点的数值来保证填表的结果是正确的,以及新表和旧表的映射关系的维护,也就是下标的变化。

填表顺序。填当前状态的时候,所需要的状态应当已经计算过了。还是斐波那契数列,填dp[4]的时候,dp[3]和dp[2]应当都已经计算好了,那么dp[4]也就出来了,此时的顺序就是从左到右。还有别的顺序,要依据前面的分析来决定。

返回值,要看题目要求。

优化直接写在代码上

2、组合总和Ⅳ

377. 组合总和 Ⅳ

C++算法 —— 动态规划(12)两道小题_第1张图片
看似像是完全背包问题,但有所不同。同样的数字,不同的顺序,就不是一种个数,比如示例中的112三个数字。所以这不是背包问题,也不是组合,而是求排列。

这时候的状态表示只能在分析子问题的过程中,发现重复的子问题,然后得到状态表示。对于target,先选中一个数a,有好几种选法,然后剩下的就是target - a,再在这个目标下,再去选一个位置,所以我们可以这样表示,dp[i]是凑成总和为i的排列数个数。让最后一个位置的数为nums[j],那么总和为i的话,前面的就应该选择i - nums[j],不过需要i >= nums[j]才行,然后dp[i] += dp[i - nuns[j]]就行。

初始化,dp[0] = 1,因为总和是0,只有空集才行。填表顺序是从左到右。返回值是最后一个值。

    int combinationSum4(vector<int>& nums, int target) {
        vector<double> dp(target + 1);//只能是double,更小的long long不行
        dp[0] = 1;
        for(int i = 1; i <= target; i++)
        {
            for(auto x : nums)
            {
                if(i >= x)
                    dp[i] += dp[i - x];
            }
        }
        return dp[target];
    }

3、卡特兰数

96. 不同的二叉搜索树

C++算法 —— 动态规划(12)两道小题_第2张图片

假设5个节点,12345,以3号节点为根节点,那么左子树可以有2个数12,右子树有两个数45,也可以让4作为根节点,然后计算有多少种树的构成。所以就让dp[i]表示节点个数为i时有多少个二叉搜索树。

假设j为根节点,那么就变成两个区间1 ~ j - 1,j + 1 ~ i,那么这两个区间总共的个数应当是dp[j - 1] 乘 dp[i - j],左边区间j - 1个节点,右边区间i - j个区间。所以dp[i] += dp[j - 1] * dp[i - j],而这个式子就是卡特兰数。

初始化,需要用到i - 1,j - 1,要初始化dp[0],空树也是一种二叉搜索树,所以就是1。

    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[i - j] * dp[j - 1];
            }
        }
        return dp[n];
    }

结束。

你可能感兴趣的:(C++算法,算法,c++,动态规划)