欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

组合数

程序员文章站 2022-06-08 12:26:29
...

灵感来源: 算法课

一、定义及性质

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

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

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

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

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

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

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

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

    • rr 个元素中不含元素 ii,那么需要从剩余的元素中选出 rr 个元素,可能的数目为 CnrC_n^{r}.
    • rr 个元素中含有元素 ii,那么只需从剩余的元素中选出 r1r-1 个元素即可,可能的数目为 Cnr1C_n^{r-1}.

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

二、计算方法

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

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

哦?那你想说什么?

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

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

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

组合数

其中 ii 表示组合数中的上标,jj 表示下标.

之后给出所有的依赖关系

组合数

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

组合数

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

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

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