【动态规划算法】(似包非包以及卡特兰数问题深入解析)


🔥承渊政道: 个人主页
❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》 《C++知识内容》 《Linux系统知识》 《算法刷题指南》 《测评文章活动推广》 《大模型语言路线学习》
✨逆境不吐心中苦,顺境不忘来时路!✨ 🎬 博主简介:

在动态规划问题中,背包问题一直是非常经典的一类模型.很多题目表面上看起来都像是在做"选择":某个物品选不选、某个状态能不能由之前的状态转移而来、某种方案能否通过累加得到.因此,初学者在遇到一些计数类、组合类或者划分类问题时,往往会下意识地把它们归类为背包问题.但在实际刷题过程中,我们会发现有些题目虽然形式上"像背包",却并不完全符合传统背包模型.这类问题可以称为"似包非包"问题.它们可能同样涉及状态转移、方案统计、组合构造,但其核心并不是简单的"物品选择"和"容量限制",而是更强调结构划分、状态依赖顺序、区间组合关系或者递推规律.如果强行套用背包模板,往往会导致状态定义不准确,甚至无法写出正确的转移方程.与此同时,卡特兰数问题也是动态规划和组合数学中非常重要的一类问题.它常出现在括号匹配、二叉搜索树个数、出栈序列、路径规划、凸多边形划分等经典场景中.这些问题看似背景不同,但背后都有相似的递推结构:一个整体问题可以被拆分成左右两个子问题,并通过枚举分割点来累加所有可能方案.这种思想与动态规划中的"拆分子问题、合并结果"高度一致.通过本文的学习,希望你不仅能够掌握具体题目的解法,更能够提升动态规划建模能力:看到题目,能够判断它到底是不是背包模型;遇到组合计数问题时,能够分析其内在结构;面对卡特兰数相关问题,能够快速识别其递推特征.这样在后续解决更复杂的动态规划问题时,就不会只停留在"套模板"的层面,而是能够真正理解状态设计与转移逻辑背后的本质.废话不多说,下面跟着小编的节奏🎵一起去疯狂的学习吧!

目录

1.似包非包问题背景介绍

在动态规划专题中,背包问题是非常经典、也非常重要的一类模型.无论是01背包、完全背包,还是多重背包,本质上都围绕一个核心思想展开:在有限条件下,对若干个物品进行选择,并通过状态转移求出最优值或方案数.

正因为背包模型影响很大,很多人在学习动态规划时,遇到一些"选择类""计数类""组合类"问题时,往往会第一时间想到背包.比如题目中出现"能否组成某个目标值""有多少种方案""把一个数拆成若干部分""从若干状态中累加结果"等描述时,很容易让人联想到背包问题.

但是,在实际刷题过程中,我们会发现有些题目虽然表面上看起来很像背包,但本质却并不完全属于背包模型.这类题目就可以称为"似包非包问题".

所谓"似包非包",指的是题目在形式上具有一定的背包特征,比如都涉及状态选择、方案累加、目标值构造等,但它的核心建模方式并不是传统背包中的"物品选择 + 容量限制".换句话说,它看起来像是在做背包,实际上更强调的是递推关系、结构划分、状态组合或者数学规律.

例如,传统背包问题通常有比较清晰的"物品"和"容量"两个维度.我们会思考第i个物品选不选,当前容量j能否由之前的状态转移而来.而在似包非包问题中,题目可能并没有明确的物品概念,也没有真正意义上的背包容量.它可能是在求一种数字拆分方案、一种排列组合数量,或者某种特殊结构的生成方式.

这类问题的难点在于:如果只看题目表面,很容易误以为它可以直接套用背包模板.但如果没有真正理解状态含义,就可能出现状态定义不准确、循环顺序错误、转移方程混乱等问题.尤其是在计数类动态规划中,循环顺序、状态含义和转移来源稍有不同,最终答案就可能完全不一样.

比如有些问题看似是在"凑目标值",但实际上并不是从一组物品中选择,而是在按照某种递推规则构造结果;有些问题看似是在统计方案数,但它统计的不是组合方案,而是有序排列方案;还有些问题虽然也存在"累加状态"的过程,但其本质是数学递推,并不是背包中的容量转移.

因此,学习"似包非包问题"的关键,不在于记住某一个固定模板,而在于学会判断题目的本质.我们需要先分析题目到底是在解决什么问题:是物品选择问题?是数字拆分问题?是方案计数问题?还是结构构造问题?只有明确了问题类型,才能进一步设计正确的状态表示和转移方程.

从动态规划学习的角度来看,似包非包问题非常适合用来提升建模能力.它能够帮助我们跳出"看到计数就套背包""看到目标值就写容量"的固定思维,转而更加关注状态本身的含义、子问题之间的关系,以及答案是如何一步步递推出来的.

总的来说,似包非包问题是动态规划中一类很有代表性的过渡型题目.它既和背包问题有一定联系,又不完全受限于背包模型.掌握这类问题,有助于我们更加深入地理解动态规划的本质:不是机械套模板,而是根据题目特征,找到合适的状态定义、转移方式和求解顺序.


2.组合总和Ⅳ(OJ题)


算法思路:解法(动态规划):

一定要注意,我们的背包问题本质上求的是组合数问题,而这一道题求的是排列数问题.因此我们不能被这道题给迷惑,还是用常规的dp思想来解决这道题.

1.状态表示:

这道题的状态表示就是根据拆分出相同子问题的方式,抽象出来一个状态表示:

当我们在求 target 这个数一共有几种排列方式的时候,对于最后一个位置,如果我们拿出数组中的一个数 x,接下来就是去找 target - x 一共有多少种排列方式.

因此我们可以抽象出来一个状态表示:
dp[i] 表示:总和为 i 的时候,一共有多少种排列方案.

2.状态转移方程:

对于 dp[i],我们根据最后一个位置划分,我们可以选择数组中的任意一个数 nums[j],其中 0 <= j <= n - 1.

nums[j] <= target 的时候,此时的排列数等于我们先找到 target - nums[j] 的方案数,然后在每一个方案后面加上一个数字 nums[j] 即可.

因为有很多个 j 符合情况,因此我们的状态转移方程为:dp[i] += dp[target - nums[j]],其中 0 <= j <= n - 1.

3.初始化:

当和为 0 的时候,我们可以什么都不选,空集一种方案,因此 dp[0] = 1.

4.填表顺序:

根据状态转移方程易得从左往右.

5.返回值:

根据状态表示,我们要返回的是 dp[target] 的值.






核心代码

cpp 复制代码
//核心思路:动态规划,求元素可重复选取、顺序不同视为不同组合的方案数
class Solution
{
public:
    //nums:可选的正整数数组
    //target:需要凑出的目标值
    //返回值:总和为 target 的组合数量
    int combinationSum4(vector<int>& nums, int target)
    {
        //定义dp数组:dp[i] 表示凑成数字 i 的组合总数
        //数组长度为 target+1,覆盖 0 ~ target 所有数值
        //使用double类型,避免计算过程中整数溢出
        vector<double> dp(target + 1);
        
        //动态规划边界条件:凑成数字0的组合数只有1种(不选择任何数字)
        dp[0] = 1;

        //外层循环:遍历 1 ~ target,依次计算每个数字的组合数
        for(int i = 1; i <= target; i++)
        {
            //内层循环:遍历数组中的每一个数字
            for(auto x : nums)
            {
                //只有当前数字 x 小于等于 i 时,才能用来凑出数字 i
                if(x <= i)
                {
                    //状态转移方程:
                    //凑成i的组合数 = 累加「选x后,凑成 i-x 的组合数」
                    dp[i] += dp[i - x];
                }
            }
        }

        //dp[target] 即为凑成目标值target的总组合数
        return dp[target];
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

class Solution {
public:
    // nums:可选的正整数数组
    // target:需要凑出的目标值
    // 返回值:总和为 target 的组合数量
    int combinationSum4(vector<int>& nums, int target) {
        // 定义dp数组:dp[i] 表示凑成数字 i 的组合总数
        // 数组长度为 target+1,覆盖 0 ~ target 所有数值
        // 使用double类型,避免计算过程中整数溢出
        vector<double> dp(target + 1);

        // 动态规划边界条件:凑成数字0的组合数只有1种(不选择任何数字)
        dp[0] = 1;

        // 外层循环:遍历 1 ~ target,依次计算每个数字的组合数
        for (int i = 1; i <= target; i++) {
            // 内层循环:遍历数组中的每一个数字
            for (auto x : nums) {
                // 只有当前数字 x 小于等于 i 时,才能用来凑出数字 i
                if (x <= i) {
                    // 状态转移方程:
                    // 凑成i的组合数 = 累加「选x后,凑成 i-x 的组合数」
                    dp[i] += dp[i - x];
                }
            }
        }

        // dp[target] 即为凑成目标值target的总组合数
        return dp[target];
    }
};

int main() {
    Solution solution;

    // 测试用例1
    vector<int> nums1 = {1, 2, 3};
    int target1 = 4;
    cout << "测试用例1:" << endl;
    cout << "nums = {1, 2, 3}, target = 4" << endl;
    cout << "组合数量为:" << solution.combinationSum4(nums1, target1) << endl;
    cout << "期望结果:7" << endl;
    cout << endl;

    // 测试用例2
    vector<int> nums2 = {9};
    int target2 = 3;
    cout << "测试用例2:" << endl;
    cout << "nums = {9}, target = 3" << endl;
    cout << "组合数量为:" << solution.combinationSum4(nums2, target2) << endl;
    cout << "期望结果:0" << endl;
    cout << endl;

    // 测试用例3
    vector<int> nums3 = {1};
    int target3 = 5;
    cout << "测试用例3:" << endl;
    cout << "nums = {1}, target = 5" << endl;
    cout << "组合数量为:" << solution.combinationSum4(nums3, target3) << endl;
    cout << "期望结果:1" << endl;
    cout << endl;

    // 测试用例4
    vector<int> nums4 = {2, 3, 5};
    int target4 = 8;
    cout << "测试用例4:" << endl;
    cout << "nums = {2, 3, 5}, target = 8" << endl;
    cout << "组合数量为:" << solution.combinationSum4(nums4, target4) << endl;
    cout << "期望结果:6" << endl;
    cout << endl;

    return 0;
}

3.卡特兰数问题背景介绍

卡特兰数(Catalan numbers)是一类在组合数学中非常经典的数列,常用于计数具有 递归结构合法匹配关系前缀约束条件 的问题.

它最早与欧拉研究多边形剖分问题有关,后来由比利时数学家欧仁·卡特兰系统研究,因此得名.

卡特兰数通常记为 C n C_n Cn,前几项为:

1 , 1 , 2 , 5 , 14 , 42 , 132 , 429 , ... 1,\ 1,\ 2,\ 5,\ 14,\ 42,\ 132,\ 429,\dots 1, 1, 2, 5, 14, 42, 132, 429,...

其中:

C 0 = 1 , C 1 = 1 , C 2 = 2 , C 3 = 5 C_0=1,\quad C_1=1,\quad C_2=2,\quad C_3=5 C0=1,C1=1,C2=2,C3=5

卡特兰数的通项公式为:

C n = 1 n + 1 ( 2 n n ) C_n=\frac{1}{n+1}\binom{2n}{n} Cn=n+11(n2n)

也可以写成:

C n = ( 2 n n ) − ( 2 n n + 1 ) C_n=\binom{2n}{n}-\binom{2n}{n+1} Cn=(n2n)−(n+12n)

它的递推公式为:

C 0 = 1 C_0=1 C0=1

C n + 1 = ∑ i = 0 n C i C n − i C_{n+1}=\sum_{i=0}^{n} C_iC_{n-i} Cn+1=i=0∑nCiCn−i

1.合法括号匹配问题

一个经典的卡特兰数问题是:

给定 n n n 对括号,问有多少种合法的括号排列方式?

所谓合法括号排列,是指任意前缀中,左括号数量都不少于右括号数量,并且最终左右括号数量相等.

例如,当 n = 3 n=3 n=3 时,合法括号排列共有 5 种:

  • ((()))
  • (()())
  • (())()
  • ()(())
  • ()()()

因此答案为:

C 3 = 5 C_3=5 C3=5

这个问题中,每一个左括号可以看作一次"开始",每一个右括号可以看作一次"结束".要求在任意时刻,结束次数不能超过开始次数.

2.出栈序列问题

另一个典型问题是栈的出栈序列问题:

有 n n n 个元素按照 1 , 2 , 3 , ... , n 1,2,3,\dots,n 1,2,3,...,n 的顺序依次入栈,问可能产生多少种不同的出栈序列?

答案也是第 n n n 个卡特兰数:

C n C_n Cn

例如,当 n = 3 n=3 n=3 时,元素 1 , 2 , 3 1,2,3 1,2,3 依次入栈,可能的出栈序列共有 5 种:

  • 123
  • 132
  • 213
  • 231
  • 321

所以:

C 3 = 5 C_3=5 C3=5

这个问题和括号匹配问题本质相同:

  • 入栈可以看作左括号;
  • 出栈可以看作右括号;
  • 任意时刻,出栈次数不能超过入栈次数.

也就是说,栈中没有元素时不能执行出栈操作,这正是卡特兰数中的"前缀合法性约束".

3.不越过对角线的路径问题

卡特兰数还可以用来解决路径计数问题:

从点 ( 0 , 0 ) (0,0) (0,0) 走到点 ( n , n ) (n,n) (n,n),每次只能向右或向上走一步,要求路径不能越过主对角线 y = x y=x y=x,问有多少种走法?

答案为:

C n C_n Cn

如果把向右走看作一种操作,把向上走看作另一种操作,那么"不越过对角线"就等价于在任意前缀中,某一种操作的次数不能超过另一种操作的次数.

这个问题与合法括号匹配问题具有相同的结构.

4.二叉树结构数量

卡特兰数还可以用于计算不同二叉树结构的数量.

问题如下:

有 n n n 个节点,能够构成多少种不同形态的二叉树?

答案是:

C n C_n Cn

例如,当 n = 3 n=3 n=3 时,可以构造出 5 种不同形态的二叉树,因此:

C 3 = 5 C_3=5 C3=5

如果讨论的是由 n n n 个不同键值构成的二叉搜索树,那么不同二叉搜索树的结构数量同样是:

C n C_n Cn

原因是可以枚举根节点.假设根节点左边有 i i i 个节点,右边有 n − 1 − i n-1-i n−1−i 个节点,那么左子树有 C i C_i Ci 种结构,右子树有 C n − 1 − i C_{n-1-i} Cn−1−i 种结构,因此总数满足递推关系:

C n = ∑ i = 0 n − 1 C i C n − 1 − i C_n=\sum_{i=0}^{n-1}C_iC_{n-1-i} Cn=i=0∑n−1CiCn−1−i

这正是卡特兰数的递推形式.

5.多边形三角剖分问题

卡特兰数最早的重要应用之一是多边形剖分问题:

一个凸 ( n + 2 ) (n+2) (n+2) 边形,可以通过不相交的对角线划分成若干个三角形,问有多少种不同的划分方式?

答案为:

C n C_n Cn

例如,一个凸五边形可以被划分成三角形的方式有:

C 3 = 5 C_3=5 C3=5

这个问题也具有递归结构.选定一条边或一个三角形后,原来的多边形会被分成若干更小的多边形,而这些小问题的答案仍然由卡特兰数描述.

6.卡特兰数问题的共同特征

虽然卡特兰数可以出现在很多不同的问题中,但这些问题通常具有一些共同特征.

(1)具有前缀合法性约束

例如合法括号问题中,任意前缀必须满足:

左括号数量 ≥ 右括号数量 左括号数量 \ge 右括号数量 左括号数量≥右括号数量

出栈序列问题中,任意时刻必须满足:

入栈次数 ≥ 出栈次数 入栈次数 \ge 出栈次数 入栈次数≥出栈次数

路径问题中,路径不能越过对角线,也可以转化为类似的前缀约束.

(2)具有递归分解结构

许多卡特兰数问题都可以被拆分成左右两个子问题.

例如二叉树问题中,选择一个根节点后,剩下的节点会被分成左子树和右子树:

C n = ∑ i = 0 n − 1 C i C n − 1 − i C_n=\sum_{i=0}^{n-1}C_iC_{n-1-i} Cn=i=0∑n−1CiCn−1−i

这说明一个大问题可以被拆成两个规模更小的问题,并且总数可以通过所有拆分方式累加得到.

(3)具有成对匹配关系

卡特兰数问题经常涉及"配对"或"匹配":

  • 左括号与右括号匹配;
  • 入栈与出栈匹配;
  • 路径中的两类操作相互约束;
  • 二叉树中的左右子树递归匹配;
  • 多边形中的划分结构相互对应.

7.例题分析

下面通过一个简单例题进一步理解卡特兰数的使用.

例题:合法括号序列数量

题目:

给定 n n n 对括号,求能够组成多少种合法括号序列。

分析:

每一个合法括号序列都满足:

1.左括号和右括号数量都为 n n n;

2.任意前缀中,左括号数量不少于右括号数量.

例如,当 n = 3 n=3 n=3 时,合法序列有:

  • ((()))
  • (()())
  • (())()
  • ()(())
  • ()()()

因此答案为:

C 3 = 5 C_3=5 C3=5

一般情况下,答案为:

C n = 1 n + 1 ( 2 n n ) C_n=\frac{1}{n+1}\binom{2n}{n} Cn=n+11(n2n)

8.代码实现

在实际编程中,可以使用递推公式计算卡特兰数.

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int main() {
    int n;
    cin >> n;

    vector<long long> C(n + 1, 0);
    C[0] = 1;

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

    cout << C[n] << endl;

    return 0;
}

递推公式为:

C n = ∑ i = 0 n − 1 C i C n − 1 − i C_n=\sum_{i=0}^{n-1}C_iC_{n-1-i} Cn=i=0∑n−1CiCn−1−i

其中:

  • C i C_i Ci 表示左子问题的方案数;
  • C n − 1 − i C_{n-1-i} Cn−1−i 表示右子问题的方案数;
  • 枚举所有可能的划分方式后,将结果累加.

9.总结

卡特兰数是一类非常重要的组合计数数列,其核心思想是:

用来计数具有递归分解结构、成对匹配关系以及前缀合法性约束的问题.

它的通项公式为:

C n = 1 n + 1 ( 2 n n ) C_n=\frac{1}{n+1}\binom{2n}{n} Cn=n+11(n2n)

它的递推公式为:

C 0 = 1 , C n + 1 = ∑ i = 0 n C i C n − i C_0=1,\qquad C_{n+1}=\sum_{i=0}^{n}C_iC_{n-i} C0=1,Cn+1=i=0∑nCiCn−i

卡特兰数的本质并不是某一个具体问题,而是一类问题的共同计数模型.只要问题中存在"前缀不能非法""左右结构递归组合"或者"成对匹配"的特征,就很有可能与卡特兰数有关.


4.不同的二叉搜索树(OJ题)


算法思路:解法(动态规划):

这道题属于卡特兰数的一个应用,同样能解决的问题还有合法的进出栈序列、括号匹配的括号序列、电影购票等等.

1.状态表示:

这道题的状态表示就是根据拆分出相同子问题的方式,抽象出来一个状态表示:

当我们在求个数为 n 的 BST 的个数的时候,当确定一个根节点之后,左右子树的结点个数也确定了.此时左右子树就会变成相同的子问题,因此我们可以这样定义状态表示:
dp[i] 表示:当结点的数量为 i 个的时候,一共有多少颗 BST.

难的是如何推导状态转移方程,因为它跟我们之前常见的状态转移方程不是很像.

2.状态转移方程:

对于 dp[i],此时我们已经有 i 个结点了,为了方便叙述,我们将这 i 个结点排好序,并且编上 1, 2, 3, 4, 5......i 的编号.

那么,对于所有不同的 BST,我们可以按照下面的划分规则,分成不同的 i 类:按照不同的头结点来分类.分类结果就是:

i. 头结点为 1 号结点的所有 BST

ii. 头结点为 2 号结点的所有 BST

iii. ......

如果我们能求出每一类中的 BST 的数量,将所有类的 BST 数量累加在一起,就是最后结果.

接下来选择头结点为 j 号的结点,来分析这 i 类 BST 的通用求法.

如果选择j 号结点来作为头结点,根据 BST 的定义:

i. j 号结点的左子树的结点编号应该在 [1, j - 1] 之间,一共有 j - 1 个结点.那么 j 号结点作为头结点的话,它的左子树的种类就有 dp[j - 1] 种(回顾一下我们 dp 数组的定义哈);

ii. j 号结点的右子树的结点编号应该在 [j + 1, i] 之间,一共有 i - j 个结点.那么 j 号结点作为头结点的话,它的右子树的种类就有 dp[i - j] 种;

根据排列组合的原理可得:j 号结点作为头结点的 BST 的种类一共有 dp[j - 1] * dp[i - j] 种!

因此,我们只要把不同头结点的 BST 数量累加在一起,就能得到 dp[i] 的值:dp[i] += dp[j - 1] * dp[i - j](1 <= j <= i),注意用的是 +=,并且 j1 变化到 i.

3.初始化:

我们注意到,每一个状态转移里面的 j - 1i - j 都是小于 i 的,并且可能会用到前一个的状态(当 i = 1, j = 1 的时候,要用到 dp[0] 的数据).因此要先把第一个元素初始化.

i = 0 的时候,表示一颗空树,空树也是一颗二叉搜索树,因此 dp[0] = 1.

4.填表顺序:

根据状态转移方程,易得从左往右.

5.返回值:

根据状态表示,我们要返回的是 dp[n] 的值.







核心代码

cpp 复制代码
//题目:给定整数 n,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种
class Solution
{
public:
    //n:节点总数
    //返回值:n个节点能组成的不同二叉搜索树的数量
    int numTrees(int n)
    {
        //1.创建dp数组
        //dp[i]:表示由 i 个节点组成的互不相同的二叉搜索树的数量
        //数组长度 n+1,下标范围 0~n,初始值全为0
        vector<int> dp(n + 1, 0);
        
        //2.初始化边界条件
        //0个节点:空树,也是1种合法的二叉搜索树(递归的基础条件)
        dp[0] = 1;

        //3.外层循环:枚举节点总数 i,从1到n,依次计算每个数量的结果
        for (int i = 1; i <= n; i++)
        {
            //4.内层循环:枚举以 j 作为当前根节点
            //二叉搜索树特性:左子树节点 < 根节点 < 右子树节点
            //根为j时,左子树有 j-1 个节点,右子树有 i-j 个节点
            for (int j = 1; j <= i; j++)
            {
                //5.状态转移方程(核心)
                //左子树的种类数 × 右子树的种类数 = 以j为根的总种类数
                //累加所有根节点的情况,就是i个节点的总种类数
                dp[i] += dp[j - 1] * dp[i - j];
            }
        }

        //6.返回结果:n个节点的二叉搜索树数量
        return dp[n];
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

// 二叉搜索树有多少种
class Solution
{
public:
    // n:节点总数
    // 返回值:n 个节点能组成的不同二叉搜索树的数量
    int numTrees(int n)
    {
        // 1. 创建 dp 数组
        // dp[i] 表示由 i 个节点组成的互不相同的二叉搜索树的数量
        vector<int> dp(n + 1, 0);

        // 2. 初始化边界条件
        // 0 个节点:空树,也是 1 种合法的二叉搜索树
        dp[0] = 1;

        // 3. 外层循环:枚举节点总数 i
        for (int i = 1; i <= n; i++)
        {
            // 4. 内层循环:枚举以 j 作为根节点
            for (int j = 1; j <= i; j++)
            {
                // 左子树有 j - 1 个节点
                // 右子树有 i - j 个节点
                dp[i] += dp[j - 1] * dp[i - j];
            }
        }

        // 5. 返回结果
        return dp[n];
    }
};

void runTest(Solution& solution, int n, int expected)
{
    int result = solution.numTrees(n);

    cout << "n = " << n << endl;
    cout << "不同二叉搜索树数量 = " << result << endl;
    cout << "期望结果 = " << expected << endl;

    if (result == expected)
    {
        cout << "测试通过" << endl;
    }
    else
    {
        cout << "测试失败" << endl;
    }

    cout << "------------------------" << endl;
}

int main()
{
    Solution solution;

    // 测试用例1:0个节点
    // 空树也算一种
    runTest(solution, 0, 1);

    // 测试用例2:1个节点
    runTest(solution, 1, 1);

    // 测试用例3:2个节点
    // 两种结构:
    // 1 为根,2 为右子树
    // 2 为根,1 为左子树
    runTest(solution, 2, 2);

    // 测试用例4:3个节点
    // 经典示例,结果为5
    runTest(solution, 3, 5);

    // 测试用例5:4个节点
    runTest(solution, 4, 14);

    // 测试用例6:5个节点
    runTest(solution, 5, 42);

    // 测试用例7:6个节点
    runTest(solution, 6, 132);

    // 测试用例8:7个节点
    runTest(solution, 7, 429);

    // 测试用例9:8个节点
    runTest(solution, 8, 1430);

    // 测试用例10:9个节点
    runTest(solution, 9, 4862);

    // 测试用例11:10个节点
    runTest(solution, 10, 16796);

    return 0;
}


🚀真正的勇者不是流泪的人,而是含泪奔跑的人!


敬请期待下一篇文章内容:动态规划算法的内容到这里就圆满结束啦!小编开始继续学习另外的算法领域,不断提高自己的算法能力,喜欢小编的可以继续跟着我的步伐一起继续前行!


每日心灵鸡汤:相遇是旷野,不是轨道
有人说:"人生所有的遇见,都是命中注定的伏笔."就像风遇见云,花遇见光,我遇见你时,恰好撞碎了漫天星光,我们曾并肩走过晨昏,在烟火人间里交换心事,以为这样的相伴会是岁月长情的答案.可后来才懂,人与人之间的缘分,一半是恰逢其时,一半是适可而止.我习惯在既定的轨道上稳步前行,以为岸是归宿,所有的奔波所有的奔波的终点,你却向往无边的自由,像风一样不被定义,像海一样藏着万千可能.我们就像两颗轨迹不同的星,相遇时璀璨夺目,分开时也该体面从容.就像那句戳中无数人的话:"我是行者止于岸,你是蔚然无尽蓝."行者有行者的坚守,蓝海有蓝海的辽阔.我们终将在各自的世界里熠熠生辉.

相关推荐
fangzt20101 小时前
从零搭建自动驾驶中间件(四):数据录制与回灌——算法调试的核心基础设施
算法·中间件·自动驾驶
楼田莉子1 小时前
仿Muduo的高并发服务器:基于Tcp协议的回显服务器
linux·服务器·c++·后端
人道领域1 小时前
【LeetCode刷题日记】二叉树层序遍历完全指南:从基础到LeetCode实战一篇搞定BFS模板,秒杀4道经典面试题
java·开发语言·数据结构·leetcode·面试·二叉树
Bechamz1 小时前
大数据开发学习Day28
大数据·学习
m0_614619061 小时前
独立开发者 0 元启动包:网站、数据库、部署全搞定
笔记·学习
搬砖的小码农_Sky1 小时前
比特币区块链:SHA256哈希函数
算法·区块链·哈希算法
CSCN新手听安2 小时前
【Qt】系统相关(二)鼠标事件的处理,鼠标的按下,释放,双击,移动,滚轮滚动事件的处理
开发语言·c++·qt
承渊政道2 小时前
【动态规划算法】(一文讲透二维费用的背包问题)
数据结构·c++·学习·算法·leetcode·动态规划·哈希算法
S1998_1997111609•X2 小时前
论述情况盀导致系统应用通信通讯协议被恶意注入污染蜜罐开元盀用于非法侵入爬虫植入ssd的通用技术原理
网络·网络协议·百度·哈希算法·开闭原则