浅谈组合数

本文介绍了组合数的定义,阐述了其性质,包括Cnr=Cnn-r和组合数的总和等于2^n。同时,文章讨论了如何通过循环迭代的方法计算组合数,提出了一种降低空间复杂度的计算策略,最后给出了按行计算的代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

灵感来源: 算法课

一、定义及性质

想必大家高中时就已接触过组合数,组合数是这样定义的:
  从含有 n n n 个元素的集合中,挑选出 r r r 个元素组成子集,不考虑元素的排列次序,所能形成的子集的个数就称为组合数,记为 C n r \displaystyle C_n^r Cnr,计算公式如下:
C n r = n ! r ! ( n − r ) ! C_n^r = \frac{n!}{r!(n-r)!} Cnr=r!(nr)!n!

关于组合数,有很多巧妙的性质

  1. 根据定义不难得出 C n r = C n n − r C_n^r = C_n^{n-r} Cnr=Cnnr.

  2. 考虑 C n 0 + C n 1 + C n 2 + ⋯ + C n n C_n^0+C_n^1+C_n^2+\cdots+C_n^n Cn0+Cn1+Cn2++Cnn 的值,这是什么意思呢?
    不难看出,这就是集合的全部子集个数(含空集). 如果用
    x j = { 1 ,  第  j  个元素被选取 0 ,  otherwise j = 1 , 2 , ⋯   , n x_j = \begin{cases} 1,\textrm{ 第 $j$ 个元素被选取}\\ 0,\text{ otherwise} \end{cases} j = 1,2,\cdots,n xj={1,  j 个元素被选取0, otherwisej=1,2,,n

    来表示每个元素被选取的情况,可知,每个元素都有两种可能,所以一共有 2 n 2^n 2n 种可能,即 C n 0 + C n 1 + C n 2 + ⋯ + C n n = 2 n C_n^0+C_n^1+C_n^2+\cdots+C_n^n = 2^n Cn0+Cn1+Cn2++Cnn=2n.

  3. C n + 1 r = C n r + C n r − 1 C_{n+1}^r = C_n^r + C_n^{r-1} Cn+1r=Cnr+Cnr1. 下面来解释一下:

    首先将 n + 1 n+1 n+1 个元素分为两部分:元素 i i i 和其他元素( i i i 为任意给定的 1 1 1 n n n 之间的数).

    C n + 1 r C_{n+1}^r Cn+1r 是指从 n + 1 n+1 n+1 个元素中选出 r r r 个元素的可能组合数目. 分为两种情况考虑:

    • r r r 个元素中不含元素 i i i,那么需要从剩余的元素中选出 r r r 个元素,可能的数目为 C n r C_n^{r} Cnr.
    • r r r 个元素中含有元素 i i i,那么只需从剩余的元素中选出 r − 1 r-1 r1 个元素即可,可能的数目为 C n r − 1 C_n^{r-1} Cnr1.

​ 所以,总的可能数 C n + 1 r = C n r + C n r − 1 C_{n+1}^r = C_n^r + C_n^{r-1} Cn+1r=Cnr+Cnr1.

二、计算方法

在实际计算组合数时,当然,我们可以根据组合数的定义进行计算,即直接套用公式:
C n r = n ! r ! ( n − r ) ! C_n^r = \frac{n!}{r!(n-r)!} Cnr=r!(nr)!n!

但如果我想说的只是这样,就不会有这篇文章了.

哦?那你想说什么?

一种循环迭代的求解思路. 根据前面的性质,有
C n r = { 1 , r ∈ { 0 , n } C n − 1 r + C n − 1 r − 1 C_{n}^r = \begin{cases} 1,r\in \{0,n\}\\ C_{n-1}^r + C_{n-1}^{r-1} \end{cases} Cnr={1,r{0,n}Cn1r+Cn1r1

有了递推关系,就能直接写出递归程序,但这样做看不清背后的细节.

先将递推关系表示成表格形式(将一组依赖关系和初值给出)

其中 i i i 表示组合数中的上标, j j j 表示下标.

之后给出所有的依赖关系

可以看出, C n r C_n^r Cnr 值的计算依赖于平行四边形区域中的值. 该区域共有 r + 1 r+1 r+1 行,宽度为 n − r + 1 n-r+1 nr+1,将其重新排列为长方形:

所以有两种计算思路:一种是一行一行地计算,一种是一列一列地计算,两种方法的实现是完全一样的.
可以用一个二维数组来计算,但这样做,算法的空间复杂度为 O ( r ( n − r ) ) O(r(n-r)) O(r(nr)),可不可以再降低一点呢?

当然可以. 可以看出,计算新的一行或新的一列时只用到了前一行或者前一列的值,所以可以通过不断更新一个一维数组来实现:若是按行计算,空间复杂度为 O ( n − r ) O(n-r) O(nr);若是按列计算,空间复杂度为 O ( r ) O(r) O(r).

这里采用按行计算的方式,代码(含递归法)如下:

#include<stdio.h>
#include<stdlib.h>
#pragma warning(disable:4996)

// 1. 循环迭代计算
int cn1(n, r) 
{
	if(r < n-r)
		r = n-r;

	int *a = malloc((n-r+1)*sizeof(int));
	int i, j;
	int tmp;

	// 初始化首行的值 
	for(i = 1; i <= n-r; i++)
		a[i] = 1;

	// 依次计算第 i 行的值
	for(i = 1; i <= r; i++) 
	{
		a[1] = a[1] + 1;
		for(j = 2; j <= n-r; j++)
			a[j] = a[j] + a[j-1];
	}

	tmp = a[n-r];
	free(a);
	return tmp;
}

// 2. 递归计算
int cn2(n, r)
{
	if(r == 0 || r == n)
		return 1;
	return cn2(n-1, r) + cn2(n-1, r-1);
}

// 主程序
int main(void) 
{
	int n, r;
	int result1, result2;

	printf("请输入参数 n 和 r(空格分隔):\n");
	scanf("%d%d", &n, &r);

	result1 = cn1(n, r);
    result2 = cn2(n, r);

	printf("循环迭代法的计算结果为:%d.\n", result1);
	printf("递归法的计算结果为:%d.\n", result2);

	return 0;
}

因为 C n r = C n n − r C_n^{r} = C_n^{n-r} Cnr=Cnnr,所以,若 n − r > r n-r > r nr>r,那么我们可以按行计算 C n n − r C_n^{n-r} Cnnr,保证空间复杂度为 O ( min ⁡ { n − r , r } ) O(\min\{n-r,r\}) O(min{nr,r}).

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值