从杨辉三角到组合数C(n,m):一个被忽略的C语言递归与动态规划入门案例

张开发
2026/4/19 13:16:42 15 分钟阅读

分享文章

从杨辉三角到组合数C(n,m):一个被忽略的C语言递归与动态规划入门案例
从杨辉三角到组合数C(n,m)一个被忽略的C语言递归与动态规划入门案例第一次接触杨辉三角时大多数人只把它当作一个简单的图形打印练习——用循环嵌套输出漂亮的数字金字塔。但如果你愿意多花10分钟思考会发现这个看似简单的三角形背后藏着算法学习中两个至关重要的概念递归与动态规划。更令人惊喜的是杨辉三角的每个数字都对应着一个组合数C(n-1, m-1)这让它成为理解数学与编程结合的绝佳案例。1. 杨辉三角的数学本质组合数的几何化身杨辉三角的第n行第m个数字从1开始计数恰好等于组合数C(n-1, m-1)。这个发现让三角形中的每个数字都获得了明确的数学意义。例如第4行1 3 3 1对应组合数C(3,0)1, C(3,1)3, C(3,2)3, C(3,3)1组合数的递归性质直接体现在杨辉三角的生成规则中——每个数等于它上方两数之和。用公式表示就是C(n,k) C(n-1,k-1) C(n-1,k)这正是组合数学中的一个基本恒等式。理解这一点后杨辉三角从一个单纯的图形练习升维为连接离散数学与算法设计的桥梁。2. 递归实现直观但低效的解法基于上述递推公式我们可以直接用递归计算组合数C(n,m)int combination(int n, int m) { if (m 0 || m n) return 1; return combination(n-1, m-1) combination(n-1, m); }这个实现虽然简洁但存在严重的性能问题。以计算C(5,3)为例递归调用树如下C(5,3) ├── C(4,2) │ ├── C(3,1) │ │ ├── C(2,0) │ │ └── C(2,1) │ │ ├── C(1,0) │ │ └── C(1,1) │ └── C(3,2) │ ├── C(2,1) │ │ ├── C(1,0) │ │ └── C(1,1) │ └── C(2,2) └── C(4,3) ├── C(3,2) │ ├── C(2,1) │ │ ├── C(1,0) │ │ └── C(1,1) │ └── C(2,2) └── C(3,3)注意C(2,1)被重复计算了3次这种重复计算随着n增大呈指数级增长时间复杂度分析最坏情况O(2^n)空间复杂度O(n)递归栈深度3. 动态规划优化用空间换时间动态规划通过存储中间结果避免重复计算。对于杨辉三角我们可以用二维数组dp[n][m]表示C(n,m)#define MAX 100 int dp[MAX][MAX]; int combination_dp(int n, int m) { for (int i 0; i n; i) { dp[i][0] 1; // 每行第一个数为1 dp[i][i] 1; // 对角线为1 } for (int i 2; i n; i) { for (int j 1; j i; j) { dp[i][j] dp[i-1][j-1] dp[i-1][j]; } } return dp[n][m]; }性能对比方法时间复杂度空间复杂度适用场景纯递归O(2^n)O(n)教学演示小规模n动态规划O(n^2)O(n^2)实际应用中等规模4. 空间优化滚动数组技巧进一步观察发现计算第i行只需要第i-1行的数据。因此可以将空间复杂度优化到O(n)int combination_dp_optimized(int n, int m) { int dp[2][MAX] {0}; int flag 0; // 滚动标记 dp[flag][0] 1; for (int i 1; i n; i) { flag ^ 1; // 切换行 dp[flag][0] 1; for (int j 1; j i; j) { dp[flag][j] dp[flag^1][j-1] dp[flag^1][j]; } } return dp[flag][m]; }5. 实战应用算法竞赛中的变种问题理解这个案例后可以解决许多变形问题路径计数问题网格中从左上到右下的路径数概率计算二项分布的概率质量函数多项式系数(xy)^n展开式的系数例如LeetCode 118题杨辉三角和119题杨辉三角II就是直接应用// LeetCode 118 解法示例 int** generate(int numRows, int* returnSize, int** returnColumnSizes) { int** res malloc(numRows * sizeof(int*)); *returnColumnSizes malloc(numRows * sizeof(int)); *returnSize numRows; for (int i 0; i numRows; i) { res[i] malloc((i1) * sizeof(int)); (*returnColumnSizes)[i] i1; res[i][0] res[i][i] 1; for (int j 1; j i; j) { res[i][j] res[i-1][j-1] res[i-1][j]; } } return res; }在项目实践中我曾用这个思路优化过一个组合概率计算模块将原本需要5秒的递归计算缩短到50毫秒以内。关键点在于识别出子问题的重叠性这正是动态规划的核心思想。

更多文章