精华内容
下载资源
问答
  • 组合数排列数

    万次阅读 多人点赞 2019-03-09 15:18:09
    1 组合数排列数是什么 组合:有一个袋子,里面有10个标有1-10数字的球,问如果每次拿出3个球,一共有多少不同的组合?(1,2,3)(3,1,2)算一种,不考虑次序。 排序:有一个袋子,里面有10个标有1-10...

    1 组合数与排列数是什么

                组合:有一个袋子,里面有10个标有1-10数字的球,问如果每次拿出3个球,一共有多少不同的组合?(1,2,3)和(3,1,2)算一种,不考虑次序。

                排序:有一个袋子,里面有10个标有1-10数字的球,问如果每次拿出3个球,一共有多少不同的排序?(1,2,3)和(3,1,2)算二种,考虑次序。

    2 如何表示

                10个中取3个

                组合数表示为 n = 10  ,r = 3

                排序数表示为    n = 10  ,r = 3

                写成    

     

    3 计算公式

                 

    4剖析计算公式

                      为什么这么算就能算出组合数和排序数?

    袋子里有10个球

    拿第一次的时候,袋子里有10个球,拿到某种球有10种可能,拿到的组合数有10种,例如 1 、 2、 3.......

    拿第二次的时候,袋子里有9个球,拿到某种球有9种可能,拿到的组合数有10*9种,例如 (1,2)、(1,3)........

    拿第三次的时候,袋子里有8个球,拿到某种球有8种可能,拿到的组合数有10*9*8种,例如 (1,2,3)、(1,3,4)........

    第三次拿到的组合数是720种,但是这里面有重复的,也就是说(1,2,3)和(1,3,2)算一种组合,那有多少种重复的呢?有3的阶乘那么多个也就是6种,720/6=120   和图中算组合数的最后一个算式是一样的,排列的数量就是刚好不用去重,也就是不用除以6,等于720种。

     

    ——————————————————————慢慢学数学

     

    展开全文
  • 转载于:https://www.cnblogs.com/shengwang/p/9836236.html

    1484685-20181023140816487-817336664.png

    转载于:https://www.cnblogs.com/shengwang/p/9836236.html

    展开全文
  • matlab_排列组合

    2016-03-17 21:56:33
    整理了几个常用排列组合与阶乘等函数。希望对大家有用!
  • 入门学习Linux常用必会60个命令实例详解doc/txt

    千次下载 热门讨论 2011-06-09 00:08:45
    这是因为Linux许多版本的Unix一样,提供了虚拟控制台的访问方式,允许用户在同一时间从控制台(系统的控制台是与系统直接相连的监视器键盘)进行多次登录。每个虚拟控制台可以看作是一个独立的工作站,工作台...
  • 比如(0, 1, 2) 列出这三个数字的任意组合,组合长度为3: 000,001,002,100,101,102..... 需要一个算法,特此记录 使用递归完成,随着可选数字的增多,需要的时间也大大增加,当需要11电话号码的所有组合,则有10的10次方...

    最近要做一个密码本,列出所有电话号码的可能组合

    也就是指定个数的数字的所有组合

    比如(0, 1, 2) 列出这三个数字的任意组合,组合长度为3: 000,001,002,100,101,102.....

    需要一个算法,特此记录

    使用递归完成,随着可选数字的增多,需要的时间也大大增加,当需要11电话号码的所有组合,则有10的10次方,10000000000种可能,

    (因为电话号码第一位为1),在我的个人电脑上,我计算大致需要34个小时才能跑完,写入文件中大致有200多G

    后面要过滤(0,2,6,9)这几种可能,因为电话号码第二位不存在这几个数字,会减少不少数据

    下面是go代码的算法实现

    package main
    
    func main() {
            // 四位的所有组合
    	sli := []string{"3", "5", "7", "8"}
    	cast(sli, 0, "")
    }
    
    func cast(sli []string, s int, str string) {
    	if s != len(sli)-1 {
    		for i := 0; i < len(sli); i++ {
    			strtmp := str + sli[i]
    			cast(sli, s+1, strtmp)
    		}
    	} else {
    		for i := 0; i < len(sli); i++ {
    			println(str + sli[i])
    		}
    
    	}
    }
    

     

    展开全文
  • C++求解组合数的具体实现

    千次阅读 多人点赞 2020-09-20 12:54:34
    很少写关于具体算法的总结笔记,因为很难把一个算法...这次想总结一下组合数的具体实现,原因是最近总是碰见组合数,所以决定来写写,免得每次从头推导公式耽误时间。排列组合经常会作为一个问题解决方案中一部分...

    前言

    很少写关于具体算法的总结笔记,因为很难把一个算法从头到尾的叙述清晰并且完整,容易造成误解。这次想总结一下组合数的具体实现,原因是最近总是碰见组合数,所以决定来写写,免得每次从头推导公式耽误时间。排列组合经常会作为一个问题解决方案中一部分,通常是求某个问题有多少个解,达到某种状态有多少种操作方式等等。

    问题起因

    今天下午解一道简单题,难度简直刷新了我的认知,其中需要用到组合数,但这仅仅是解题的一小部分,没办法,从头推导的,简单优化下,写出了如下代码:

    int C(int a, int b)
    {
        int ans = 1;
        for (int i = a; i > a - b; i--) ans *= i;
        for (int i = b; i > 1; i--) ans /= i;
        return ans;
    }
    

    因为时间紧迫,范围也比较小,同时可以控制 ab 的大小,所以临时写下的这段代码可以运行,不然这段代码会出现各种错误的。

    组合公式

    既然是想做总结,还是从头来看看组合公式,根据原始公式实现算法,并尝试优化它,当熟悉这个套路之后,就可以直接拿来用了,可以节省不少时间,组合公式的常见表示方式如下:

    C n m = n ! m ! ( n − m ) ! = C n n − m , ( n ≥ m ≥ 0 ) C^m_n = \frac{n!}{m!(n-m)!} = C^{n-m}_n,(n \geq m \geq 0) Cnm=m!(nm)!n!=Cnnm,(nm0)

    这个公式写出来清晰多了,n!表示n的阶乘,计算方式为 n*(n-1)*(n-2)*(n-3)*…*3*2*1, 相信很多人都清楚,我们只要把这个数据公式翻译成代码就可以了:

    int C2(int n, int m)
    {
        int a = 1, b = 1, c = 1;
        for (int i = n; i >= 1; --i) a *= i;
        for (int i = m; i >= 1; --i) b *= i;
        for (int i = n-m; i >= 1; --i) c *= i;
        return a/(b*c);
    }
    

    代码比较简单,依次计算公式中三个数的阶乘,然后再做乘除法就可以了,但是你有没有思考过一个问题,int 类型的整数最大能表示的阶乘是多少?是12!,它的值是 479,001,600,它是 int 表示范围内最大的阶乘数,看来这种实现方式局限性很大,如果 n 大于12就没有办法计算了。

    公式变形

    实际上根据阶乘的定义,n! 和 (n-m)! 是可以约分的,将这两个式子约分后,公式可以化简为:

    C n m = n ! m ! ( n − m ) ! = n ( n − 1 ) ( n − 2 ) . . . ( n − m + 1 ) ) m ! , ( n ≥ m ≥ 0 ) C^m_n = \frac{n!}{m!(n-m)!} = \frac{n(n-1)(n-2)...(n-m+1))}{m!},(n \geq m \geq 0) Cnm=m!(nm)!n!=m!n(n1)(n2)...(nm+1)),(nm0)

    公式写成这样之后可以少计算一个阶乘,并且计算的范围也会缩小,代码实现和一开始展示的代码思想是一样的:

    int C3(int n, int m)
    {
        int a = 1, b = 1;
        for (int i = n; i > n - m; --i) a *= i;
        for (int i = m; i >= 1; i--) b *= i;
        return a/b;
    }
    

    这段代码虽然经过了化简,但是当 n 和 m 非常接近的时候,分子还是接近于 n!,所以表示的范围还是比较小。

    递推公式

    直接给出的公式经过化简后还是受制于计算阶乘的范围,得想个办法看看能不能绕过阶乘计算,方法总是有的,并且前辈们已经给我们整理好了,我们总是站在巨人的肩膀上,下面就是递推公式:

    { C n m = 1 , ( m = 0 或 m = n ) C n m = C n − 1 m + C n − 1 m − 1 , ( n > m > 0 ) \begin{cases} {C^m_n} = 1,\qquad\qquad\qquad (m=0 或 m=n) \\ {C^m_n} = {C^m_{n-1}} + {C^{m-1}_{n-1}},\qquad(n > m > 0) \end{cases} {Cnm=1,(m=0m=n)Cnm=Cn1m+Cn1m1,(n>m>0)

    递归实现

    有了上面的分段函数表示,就满足了递归的条件,既有递归调用缩小规模,也有递归出口,这样实现起来很简单,代码如下:

    int C4(int n, int m)
    {
        if (n == m || m == 0) return 1;
        return C4(n-1, m) + C4(n-1, m-1);
    }
    

    这两行代码是不是很秀?不过使用递归常常会出现一问题,那就是相同子问题多次计算,导致效率低下,这个计算组合数的方式同样存在重复计算子问题的缺点,我们以调用C4(5, 3)为例,看看下面的调用关系图:

    5,3
    4,3
    3,3
    3,2
    2,2
    2,1
    1,1
    1,0
    4,2
    3,2
    3,1
    2,2
    2,1
    2,1
    2,0
    1,1
    1,0
    1,1
    1,0

    从这个图可以清晰看出C4(3, 2)C4(2, 1) 都被计算了多次,当 m 和 n 的数字比较大的时候,会进行更多次的重复计算,严重影响计算的效率,有没有什么办法解决重复计算的问题呢?

    备忘递归

    解决重复计算的常用方法是利用一个备忘录,将已经计算式子结果存储起来,下次再遇到重复的计算时直接取上次的结果就可以了,我们可以将中间结果简单存储到map中。

    假设 n 不超过10000,这比12已经大太多了,我们可以使用 n * 10000 + m 作为map的键,然后将结果存储到map中,每次计算一个式子前先看查询备忘录,看之前有没有计算过,如果计算过直接取结果就可以了,代码简单实现如下:

    int C5(int n, int m, map<int, int>& memo)
    {
        if (n == m || m == 0) return 1;
    
        auto itora = memo.find((n-1)*10000+m);
        int a = itora != memo.end() ? itora->second : C4(n-1, m);
        if (itora == memo.end()) memo[(n-1)*10000+m] = a;
    
        auto itorb = memo.find((n-1)*10000+m-1);
        int b = itorb != memo.end() ? itorb->second : C4(n-1, m-1);
        if (itorb == memo.end()) memo[(n-1)*10000+m-1] = b;
    
        return a + b;
    }
    

    使用 map 作为备忘录可以避免重复计算,这是解决递归效率低下的常用方法,那么有了递推公式不使用递归实现可不可以呢?当然可以了,针对于这个问题,有了递推公式我们还可以使用动态规划(dp)的方式来实现。

    动态规划

    动态规划常常适用于有重叠子问题和最优子结构性质的问题,试图只解决每个子问题一次,具有天然剪枝的功能。基本思想非常简单,若要解一个给定问题,我们需要解其不同子问题,再根据子问题的解以得出原问题的解。

    再回顾一下递推公式:

    { C n m = 1 , ( m = 0 或 m = n ) C n m = C n − 1 m + C n − 1 m − 1 , ( n > m > 0 ) \begin{cases} {C^m_n} = 1,\qquad\qquad\qquad (m=0 或 m=n) \\ {C^m_n} = {C^m_{n-1}} + {C^{m-1}_{n-1}},\qquad(n > m > 0) \end{cases} {Cnm=1,(m=0m=n)Cnm=Cn1m+Cn1m1,(n>m>0)

    翻译成人话就是,当m等于0或者等于n的时候,组合数结果为1,否则组合数结果等于另外两个组合数的和,我们可以采用正向推导的方式,将 n 和 m 逐步扩大,最终得到我们想要的结果,定义dp表格如下:

    n\m(0)(1)(2)(3)(4)(5)
    (0)1
    (1)11
    (2)121
    (3)1331
    (4)1464<1>
    (5)1510==>10<5><1>

    从表格可以清晰的看出求解 C(5,3) 只需要计算5行3列(从0开始)的数据,其余的值可以不用计算,这样我们就可以对照着表格写代码啦,定义一个dp数组,然后双重for循环就搞定了:

    int C6(int n, int m)
    {
        if (n == m || m == 0) return 1;
    
        vector<vector<int>> dp(n+1, vector<int>(m+1));
        for (int i = 0; i <= n; i++)
            for (int j = 0; j <= i && j <= m; j++)
                if (i == j || j == 0) dp[i][j] = 1;
                else dp[i][j] = dp[i-1][j] + dp[i-1][j-1];
    
        return dp[n][m];
    }
    

    至此,我们就采用了非递归的方式求解出了组合数的结果,但是这里的空间有点浪费,每次都要花费O(mn)的空间复杂度,有没有办法降低一点呢?我们可以找找规律进行压缩。

    压缩DP

    观察之前的动态规划实现的代码,我们发现求解第 i行的数据时只与第 i-1 行有关,所以我们可以考虑将二维数据压缩成一维,还是逐行求解,只不过可以用一维数组来记录求解的结果,优化代码如下:

    int C7(int n, int m)
    {
        if (n == m || m == 0) return 1;
    
        vector<int> dp(m+1);
        for (int i = 0; i <= n; i++)
            for (int j = min(i, m); j >= 0; j--)
                if (i == j || j == 0) dp[j] = 1;
                else dp[j] = dp[j] + dp[j-1];
    
        return dp[m];
    }
    

    这样我们就将空间复杂度降低到了O(m),需要注意的是在计算dp时,因为采用了压缩结构,为防止前面的修改影响后续结果,所以采用里倒序遍历,这是一个易错的点。

    其他优化

    代码实现到这里,我们的时间复杂度是O(nm),空间复杂是O(m),其实还有进一步的优化空间:

    • 减小m: 因为题目是求解C(n, m),但是我们知道组合公式中,C(n, m) 和 C(n, n-m) 相等,所以当 n-m 小于 m 的时候求解C(n, n-m)可以降低时间复杂度和空间复杂度。

    • 部分剪枝: 观察函数int C7(int n, int m),实际上当i为n时,j没必要遍历到0,只需要计算j等于m的情况就可以了,可以提前计算出结果。

    • 缩小计算范围: 从上面的剪枝操作得到启示,其实每一行没必要全部计算出来,以 C(5,3) 为例,我们只需要计算出表格中有数字的位置的结果就可以了:

    n\m(0)(1)(2)(3)(4)(5)
    (0)1
    (1)11
    (2)121
    (3)331
    (4)64
    (5)==>10

    这样来看每行最多需要计算3个值,那么时间复杂度可以降低到 O(3n),去掉常数,时间复杂度降为 O(n)

    总结

    • 计算组合数可以采用逆向递归和正向递推两种方式,递归时注意写好递归出口
    • 采用正向递推方法时利用动态规划思想,使用子问题的解拼凑出最终问题的解
    • 计算组合数若使用了计算阶乘应注意范围,避免在计算时产生溢出,int最多能表示 12!
    • 使用动态规划方法时可以逐步优化空间和时间,这其实就是优化算法的过程,也是提升的过程
    • 关于组合数的求解方式,我们可以找到时间复杂度O(n)、空间复杂度O(m)的非递归解法

    补充

    感谢 @小胡同的诗 同学的补充和提醒,让我再次感受到数学力量的深不可测,原来求解组合数还有这样一个递推公式:

    { C n m = 1 , ( m = 0 或 m = n ) C n m = n − m + 1 m C n m − 1 , ( n > m > 0 ) \begin{cases} {C^m_n} = 1,\qquad\qquad\qquad (m=0 或 m=n) \\ C_n^m=\frac{n-m+1}{m}C_n^{m-1},\qquad(n > m > 0) \end{cases} {Cnm=1,(m=0m=n)Cnm=mnm+1Cnm1,(n>m>0)

    这个公式厉害就厉害在它是一个线性的,不存在分叉的情况,也就是说即使递归也不会出现重复的计算,我们简单实现一下。

    反向递归

    int C8(int n, int m)
    {
        if (n == m || m == 0) return 1;
        return C8(n, m-1) * (n-m+1) / m;
    }
    

    代码非常紧凑,也不存在重复计算的情况,当然我们也可以使用正向计算的方式来实现。

    正向递推

    int C9(int n, int m)
    {
        if (n == m || m == 0) return 1;
    
        int ans = 1;
        m = min(m, n-m);
    
        for (int i = 1; i <= m; i++) ans = ans * (n-i+1) / i;
        return ans;
    }
    

    这段代码将时间复杂度降到了O(m),空间复杂度降到了O(1),不过特定的场景还是要选择特定的实现,虽然C9函数在时间复杂度和空间复杂度上都优于 C5 函数,但是如果一个实际问题中需要用到多个组合数的时候,C5 这种采用缓存的方式可能会是更好的选择。


    ==>> 反爬链接,请勿点击,原地爆炸,概不负责!<<==

    想讲故事?没人倾听?那是因为你还未到达一个指定的高度,当你在某个领域站稳了脚跟,做出了成绩,自然有的是时间去讲故事或者“编”故事,到时候随便一句话都会被很多人奉为圭臬,甚至会出现一些鸡汤莫名其妙的从你嘴里“说”出来。在你拥有了讲故事权利的同时,批判的声音也将随之而来~

    展开全文
  • O(logn)求组合数 fac[maxn] = {1, 1, 2}; ll C(int n, int m){return (fac[n] * quickpow((fac[m] * fac[n - m]) % mod, mod - 2)) % mod;} for (int i = 3; i < maxn; ++i) fac[i] = (i * fac[i - 1]) % mod; O...
  • 特殊元素优先处理特殊位置优先考虑 例 1六人站成一排求 甲不在排头乙不在排尾的排列数 .520 C 答案 A 分析法 1:先考虑排头排尾但这两个要求相互有影响因而考虑分类 第一类乙在排头有 A 种站法 第二类乙不在排头当然...
  • 组合数有关的公式及常用求和

    万次阅读 多人点赞 2017-08-16 13:06:09
    组合数
  • 小甲鱼零基础入门学习python笔记

    万次阅读 多人点赞 2019-08-14 11:06:30
    小甲鱼老师零基础入门学习Python全套资料百度云(包括小甲鱼零基础入门学习Python全套视频+全套源码+全套PPT课件+全套课后题及Python常用工具包链接、电子书籍等)请往我的资源... 000 愉快的开始 ...
  • 排列组合

    千次阅读 2016-07-11 21:38:24
    ACM模版排列类循环排列用递归实现多重循环,本递归程序相当于n重循环,每重循环的长度为m的情况,所以输出共有m^n行。/* * 输入样例: 3 2 * 输出样例: * 0 0 0 * 0 0 1 * 0 1 0 * 0 1 1 * 1 0 0 * 1 0 1 * 1 1...
  • 排列组合计算公式简易版

    万次阅读 2018-08-30 21:15:25
    记录一下排列组合中一些重要又常用的公式。 1.0!=10!=10! = 1 2.Pmn=n(n−1)(n−2)⋯(n−m+1)=n!(n−m)!Pnm=n(n−1)(n−2)⋯(n−m+1)=n!(n−m)!P_n ^ m = n(n-1)(n-2)\cdots (n-m+1) = \frac{n!}{(n-m)!} 3.pnn=n...
  • 本文介绍了常用排列组合算法,包括全排列算法,全组合算法,m个选n个组合算法等。 2. 排列算法 常见的排列算法有: (A)字典序法 (B)递增进位制法 (C)递减进位制法 (D)邻位对换法 (E)递归法 介绍常用的两种...
  • 组合的输出  时间限制(普通/Java) : 1000 MS/ 3000 MS 运行内存限制 : 65536 KByte  总提交 : 103 测试通过 : 34
  • 它的格式是RECT r={X1,Y1,X2,Y2},X1X2是矩形的左边右边的横坐标,Y1Y2是矩形的上边下边的纵坐标,这一点rectangle()绘制空心矩形函数参数排列一致。后面的DT_CENTER | DT_VCENTER | DT_SINGLELINE就是...
  • 测验1:Python基本语法元素 知识点概要: 普遍认为Python语言诞生于1991年 ...字符串的正向递增反向递减序号体系:正向是从左到右,0到n-1,反向是从右到左,-1到-n,举例str = "csdn" # str[0]就表示字符串c...
  • 一些常见组合数

    千次阅读 2015-11-01 16:13:07
    上面的排列,不考虑其顺序。也就是说,这k个循环排列之间互相交换顺序,还是算一种。而且每个循环排列是按特定顺序的,即{A D C}与{A C D}是不同的,即有向环,按特定方向。 这里n个物品是被标号的物品,互不...
  • 前端面试题

    万次阅读 多人点赞 2019-08-08 11:49:01
    前端面试题汇总 一、HTMLCSS 21 你做的页面在哪些流览器测试过?...它Standards模式有什么区别 21 div+css的布局较table布局有什么优点? 22 img的alt与title有何异同? strong与em的异同? 22 你能...
  • 组合数求模

    千次阅读 多人点赞 2016-09-16 05:26:04
    在程序设计中,可能会碰到多种类型的计数问题,其中不少涉及到组合数的计算,所以笔者写下这么一篇文章,期望能解决一些常规的组合数求模问题。last update time : 2020-03-04
  • c语言实现排列组合

    千次阅读 2017-03-17 18:48:06
    排列组合是算法常用的基本工具,如何在c语言中实现排列组合呢?思路如下: 首先看递归实现,由于递归将问题逐级分解,因此相对比较容易理解,但是需要消耗大量的栈空间,如果线程栈空间不够,那么就运行不下去了...
  • 测试开发笔记

    万次阅读 多人点赞 2019-11-14 17:11:58
    架构师,开发工程师写出《概要设计说明书》High-level design(HLD) 内容:系统程序中的模块,子模块他们之间的关系接口 测试的工作:对HLD进行测试评审A集成测试计划《集成测试计划书》B集成测试设计《集成...
  • Linux 命令面试题

    万次阅读 多人点赞 2019-07-24 09:40:04
    1.Linux常用系统安全命令 sudo // 超级用户 su // 用于切换当前用户身份到其他身份,变更时需输入所要变更的用户账号与密码。 chmod // 用来变更文件或目录的权限 setfacl // 设置文件访问控制列表 2.Linux常用进程...
  • matlab排列组合

    千次阅读 2015-12-08 10:26:26
    matlab做排列组合:比如要ABCD的全排列(permutation),可以用perms函数  perms(['ABC']) 运行结果  CBA  CAB  BCA  BAC  ABC  ACB   >> perms([1 2 3]) ans =  3 2 1   3 1 2...
  • · 在CSS中定义的宽度高度之外绘制元素的内边距边框 border-box · 在CSS中微元素设定的宽度高度就决定了元素的边框盒 · 即为元素在设置内边距边框是在已经设定好的宽度高度之内进行绘制 · CSS中...
  • 测试开发需要学习的知识结构

    万次阅读 多人点赞 2018-04-12 10:40:58
    4、白盒测试&黑盒测试详细介绍 黑盒测试 · 等价类划分方法 · 边界值分析 · 错误推测 · 因果图方法 · 判定驱动分析方法 · 正交实验设计方法:取正交的测试用例组合 · 功能图分析方法 1)等价类划分: 把...
  • 【数据库学习】数据库总结

    万次阅读 多人点赞 2018-07-26 13:26:41
    数据库管理系统在三级模式之间提供了两层映像: 外模式/模式映像(保证数据的逻辑独立性) 模式/内模式映像(保证了物理独立性) ④ 分为临时表和永久。 临时 临时存储在tempdb中(如下),当不再使用时...
  • MATLAB中常用排列组合、阶乘函数

    万次阅读 多人点赞 2017-03-07 22:06:07
    1、求n的阶乘,方法如下: a、factorial(n) b、gamma(n+1) c、v='n!'; vpa(v) ...2、求组合(数),方法如下: ...b、nchoosek(n,m) 从n各元素中取m个元素的所有组合数。 nchoosek(x,m) 从向量x中取m个
  • MATLAB排列组合计算

    千次阅读 2013-12-14 16:27:18
    matlab做排列组合:比如要ABCD的全排列(permutation),可以用perms函数  perms(['ABC']) 运行结果  CBA  CAB  BCA  BAC  ABC  ACB   >> perms([1 2 3]) ans =  3 2 1   3 1 2...
  • c语言排列组合还可以这样求

    万次阅读 多人点赞 2017-08-19 11:07:48
    本文主要讲编程比赛中常用排列组合。 首先,排列组合的公式是(其中P代表的就是A) 最普通的算法就是按照公式求了,即分子算出来,分母算出来,然后相除,写成代码为: int c( int m,int n ) { int a = 1,b = 1,...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 69,243
精华内容 27,697
关键字:

常用排列数和组合数表