
🔥承渊政道: 个人主页
❄️个人专栏: 《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 种:
123132213231321
所以:
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),注意用的是 +=,并且 j 从 1 变化到 i.
3.初始化:
我们注意到,每一个状态转移里面的 j - 1 和 i - 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;
}


🚀真正的勇者不是流泪的人,而是含泪奔跑的人!
敬请期待下一篇文章内容:动态规划算法的内容到这里就圆满结束啦!小编开始继续学习另外的算法领域,不断提高自己的算法能力,喜欢小编的可以继续跟着我的步伐一起继续前行!
每日心灵鸡汤:相遇是旷野,不是轨道
有人说:"人生所有的遇见,都是命中注定的伏笔."就像风遇见云,花遇见光,我遇见你时,恰好撞碎了漫天星光,我们曾并肩走过晨昏,在烟火人间里交换心事,以为这样的相伴会是岁月长情的答案.可后来才懂,人与人之间的缘分,一半是恰逢其时,一半是适可而止.我习惯在既定的轨道上稳步前行,以为岸是归宿,所有的奔波所有的奔波的终点,你却向往无边的自由,像风一样不被定义,像海一样藏着万千可能.我们就像两颗轨迹不同的星,相遇时璀璨夺目,分开时也该体面从容.就像那句戳中无数人的话:"我是行者止于岸,你是蔚然无尽蓝."行者有行者的坚守,蓝海有蓝海的辽阔.我们终将在各自的世界里熠熠生辉.
