Java计算组合数以及生成组合排列

article/2025/9/23 17:38:27

前言

组合数计算

公式法

逐个相除法(错误)

逐个相除法修正版

素数幂乘法

基本公式法

平方差连乘法

组合恒等法

简单递归法

杨辉三角法

杨辉三角优化法

二进制法

组合数计算小结

获取数组的组合排列

二进制法

基本迭代法

从后向前迭代法(Matlab版本)

从后向前优化法

组合排列小结

后记


前言

        最近学习了一下排列组合的相关知识,组合数的计算虽然比较简单,但是在实现它的过程中,我获得了很多新的理解。与此同时,网上关于计算组合数的博文比较多,却对数组的组合排列的描述较少。因此写这篇博文,一是记录下自己关于全组合的理解,二是实现一些数组的组合排列算法,我主要考虑全组合的算法实现,并不涉及溢出后的大数处理。

组合数计算

        组合数的公式大家都可能比较熟悉,计算公式是C_{n}^{k} = \frac{n!}{k!*(n - k)!} = \frac{A_{n}^{k}}{A_{k}^{k}}C_{n}^{0} = 1。它还有两个性质,一个是互补性质,即C_{n}^{k} = C_{n}^{n - k},一个是组合恒等性质,即C_{n}^{k} = C_{n - 1}^{m - 1} + C_{n - 1}^{m},具体细节可以参考高中数学书,或者百度百科了解下。因为组合数存在计算公式和性质,我们的算法也由此着手,主要分两个部分展开,一个是直接使用计算公式计算,二个是使用组合恒等性质进行操作。为了后续书写方便,将 C_{n}^{k} 记作 C(n, k),将 A_{n}^{k} 记作 A(n, k)。

        因为这些方法必定会对参数n和k进行检验,为避免代码重复,我就将检验代码封装为一个检验方法,供后续使用,具体如下:

public static void checknk(int n, int k){if(k < 0 || k > n){ // N must be a positive integer.throw new IllegalArgumentException("K must be an integer between 0 and N.");}
}

公式法

        这一部分主要通过C_{n}^{k} = \frac{n!}{k!*(n - k)!} = \frac{A_{n}^{k}}{A_{k}^{k}}这个公式来实现,并对乘法和除法操作进行优化。

逐个相除法(错误)

        因为C(n, k) = A(n, k) / A(k, k),其中A(n, k)和A(k,k)的乘数数量一样,因此我就想着直接一个一个相除后再相乘,虽然考虑可能会有精度损失,但还是抱着侥幸心理,最终就写出了如下代码:

public static int nchoosek(int n, int k){checknk(n, k);k = (k > (n - k)) ? n - k : k;  // C(n, k) = C(n, n - k)if(k <= 1){  // C(n, 0) = 1, C(n, 1) = nreturn 1;}int divisor = n - k + 1;int dividend = 1;double cnk = 1.0;for(int i = 0; i < k; i++){cnk *= (double) (divisor + i) / (dividend + i);}return (int) cnk;
}

        因为double转换为int时,存在精度问题,所以上面的算法在计算C(13, 6)时就开始出现错误,如果将double改为float,将会在计算C(25, 9)时出现错误。我写这个方法的原因,是希望自己注意精度问题,不能对其存在侥幸心理,以为double精度较高就没事了。

逐个相除法修正版

        前面那个算法,因浮点型转换为整型造成了精度问题,被我认定为错误的算法,但是如果我非要用除法来实现呢,可不可以呢?我们知道C(n, k)一定是整数,所以 A(n, k) 一定能整除 A(k, k),这给了我信心。A(n, k)等于n - k + 1 :k的数据累积,把这些数据存于数组a中,而A(k, k)等于1:k的数据累积,把这些数据存于数组b中。所以我就把数组b的数据逐个取出来,在数组a中找到能整除它的数,再进行除法操作,如果因为进行了除法操作,使得a中没有任何数能整除b中的某个数值,那么我们就将该数值分解为质因数,再用a对它的每个质因数分别进行除法操作。最终的结果就是数组a全部是大于等于1的整数,再对a中数据进行累积,就是最终结果C(n, k),这样也避免了精度转换造成的问题。具体代码如下:

public static long nchoosek(int n, int k){if(n > 70 || (n == 70 && k > 25 && k < 45)){throw new IllegalArgumentException("N(" + n + ") and k(" + k + ") don't meet the requirements.");}checknk(n, k);k = k > (n - k) ? n - k : k;if(k <= 1){  // C(n, 0) = 1, C(n, 1) = nreturn k == 0 ? 1 : n;}int[] divisors = new int[k]; // n - k + 1 : nint firstDivisor = n - k + 1;for(int i = 0; i < k; i++){divisors[i] = firstDivisor + i;}outer:for(int dividend = 2; dividend <= k; dividend++){for(int i = k - 1; i >= 0; i--){int divisor = divisors[i];if(divisor % dividend == 0){divisors[i] = divisor / dividend;continue outer;}}int[] perms = factor(dividend);for(int perm : perms){for(int j = 0; j < k; j++){int divisor = divisors[j];if(divisor % perm == 0){divisors[j] = divisor / perm;break;}}}}long cnk = 1L;for(int i = 0; i < k; i++){cnk *= divisors[i];}return cnk;
}

        C(70, 26)的结果超出了long范围,限定范围即可保证正确。其中的分解质因数函数factor,请参考分解质因数。

素数幂乘法

        我们知道A(n, k)等于n + 1 - k :n的数值累积,A(k, k)等于1 : k的数值累加,因为所有整数不是素数就是合数,而合数是由素数乘积组成,因此这两个的排列结果都可以转换为素数幂的乘积。因此我们只需将n + 1 - k :n转换为素数的值以及其对应的幂数量即可,然后减去1 : k所对应的素数及其幂数量,最终计算剩余素数幂之积即可。我才用HashMap来映射素数和素数的幂。具体代码如下:

public static long nchoosek(int n, int k){if(n > 70 || (n == 70 && k > 25 && k < 45)){throw new IllegalArgumentException("N(" + n + ") and k(" + k + ") don't meet the requirements.");}checknk(n, k);k = k > (n - k) ? n - k : k;if(k <= 1){  // C(n, 0) = 1, C(n, 1) = nreturn k == 0 ? 1 : n;}HashMap<Integer, Integer> primeMap = new HashMap<>();for(int i = n - k + 1; i <= n; i++){for(int prime : factor(i)){Integer primeCount = primeMap.get(prime);primeMap.put(prime, primeCount == null ? 1 : primeCount + 1);}}for(int i = 2; i <= k; i++){for(int prime : factor(i)){primeMap.put(prime, primeMap.get(prime) - 1);}}long cnk = 1L;for(Map.Entry<Integer, Integer> entry : primeMap.entrySet()){int coef = entry.getKey();int exp = entry.getValue();if(exp > 0){cnk *= (long) Math.pow(coef, exp);}}return cnk;
}

        将累积除法转换为素数的加减法,效率得到了极大提升,求幂操作直接调用Math.pow即可,无需自己写个快速求幂算法。其中的分解质因数函数factor,请参考分解质因数。

基本公式法

        根据公式,先计算A(n, k),再计算A(k, k),最后两者相除即可。具体代码如下:

public static int nchoosek(int n, int k){if(n > 16 || (n == 16 && k == 8)){throw new IllegalArgumentException("N(" + n + ") and k(" + k + ") don't meet the requirements.");}checknk(n, k);k = (k > (n - k)) ? n - k : k;  // C(n, k) = C(n, n - k)if(k <= 1){  // C(n, 0) = 1, C(n, 1) = nreturn k == 0 ? 1 : n;}int divisor = 1;for(int i = n - k + 1; i <= n; i++){divisor *= i;}int dividend = 1;for(int i = 2; i <= k; i++){dividend *= i;}return (int) ((double) divisor / dividend);
} 

        A(16, 8) = 4151347200 > 2147483647,会发生溢出情况,造成结果错误。虽然可以利用Long来提升它的适用范围,但是该方法效率过低,此举意义不大。

平方差连乘法

        乘法相比于加减法,效率比较低,因此我们可以减少A(n, k)中乘法次数,来达到优化目的,其实质也就是阶乘的优化。关于阶乘算法的优化有很多,一部分是递归优化,一部分是阶乘结果大数的优化,这里我们并不涉及这两点,只是单纯就数学层面来优化乘法数量,这部分主要参考这篇博文:计算阶乘的另一些有趣的算法。

        1 × 2 × 3 × 4 × 5 × 6 × 7 = (4 - 3) × (4 - 2) × (4 - 1) × 4 × (4 + 1) × (4 + 2) × (4 + 3) 

                                                = (4^{2} - 1^{2}) * (4^{2} - 2^{2}) * (4^{2} - 3^{2}) * 4

       1 × 2 × 3 × 4 × 5 × 6 = (3^{2} - 1^{2}) * (3^{2} - 2^{2}) * 3^{2} * 2

       因此对于偶数长度的n阶乘的结果为:\prod_{i = 1}^{m - 1}(m^{2} - i^{2}) * m^{2} * 2, 其中m = n / 2。

              对于奇数长度的n阶乘的结果为:\prod_{i = 1}^{m - 1}(m^{2} - i^{2}) * m, 其中m = n / 2。

        与此同时,我们高中学过,奇数的前n项和为n^{2},所以上面连乘式子中的(m^{2} - i^{2})就可以优化成对奇数的减法。因此我们就可以使阶乘中乘法的操作数量下降一倍。先简要写一个factorial函数练练手,代码如下:

public static long factorial(int n){if(n < 0 || n > 20){throw new IllegalArgumentException("N must be an integer between 0 and 20.");}if(n <= 1){return 1L;}int mid = (n & 1) == 1 ? (n >> 1) + 1 : (n >> 1);int midSquare = mid * mid;long fac = (n & 1) == 1 ? mid : midSquare << 1;for(int i = 1, limit = 2 * mid - 1; midSquare > limit; i += 2){fac *= midSquare -= i;}return fac;
}

        因此我们可以用它来优化我们的A(n, k),具体代码如下所示:

public static long nchoosek(int n, int k){if(n > 30 || (n == 30 && k > 13 && k < 17)){throw new IllegalArgumentException("N(" + n + ") and k(" + k + ") don't meet the requirements.");}checknk(n, k);k = (k > (n - k)) ? n - k : k;  // C(n, k) = C(n, n - k)if(k <= 1){  // C(n, 0) = 1, C(n, 1) = nreturn k == 0 ? 1 : n;}return prod(n - k + 1, n) / prod(2, k);
}private static long prod(int begin, int end){int len = end - begin + 1;int mid = 0;long prodVal = 1L;int accumNum = 0;if((len & 1) == 1){mid = (len >> 1) + begin;prodVal = mid;accumNum = end - mid;}else{mid = (len >> 1) + begin - 1;prodVal = mid * end;accumNum = end - mid - 1;}long midSquare = (long) mid * mid;for(int i = 1; --accumNum >= 0; i += 2){prodVal *= midSquare -= i;}return prodVal;
}

        因为A(30, 14)的值超出了long范围,发生了溢出情况,造成结果错误,需要规避操作。

组合恒等法

         这一部分主要通过C_{n}^{k} = C_{n - 1}^{m - 1} + C_{n - 1}^{m}这个递推公式来实现,并对该递推公式进行优化。

简单递归法

        直接按照递推公式来实现算法,具体操作如下:

public static int nchoosek(int n, int k){if(n > 34 || (n == 34 && k >= 16 && k <= 18)){throw new IllegalArgumentException("N(" + n + ") and k(" + k + ") don't meet the requirements.");}checknk(n, k);if(k <= 1){  // C(n, 0) = 1, C(n, 1) = nreturn k == 0 ? 1 : n;}if(k > (n >>> 1)){return nchoosek(n, n - k);}return nchoosek(n - 1, k - 1) + nchoosek(n - 1, k);
}

        该方法是最容易实现的,效率也是差强人意。

杨辉三角法

        我们仔细观察这个递推公式,发现它和杨辉三角的递推公式一样,其实以前高中学习排列组合老师就讲到过这玩意。因此我们也可以使用杨辉三角来实现它。具体代码如下:

public static int nchoosek(int n, int k){if(n > 34 || (n == 34 && k >= 16 && k <= 18)){throw new IllegalArgumentException("N(" + n + ") and k(" + k + ") don't meet the requirements.");}checknk(n, k);k = (k > (n - k)) ? n - k : k;if(k <= 1){  // C(n, 0) = 1, C(n, 1) = nreturn k == 0 ? 1 : n;}int[][] yhTriangle = new int[n + 1][n + 1];for(int i = 0; i <= n; i++){yhTriangle[i][0] = 1;yhTriangle[i][i] = 1;}for(int i = 1; i <= n; i++){for(int j = 1; j <= i; j++){yhTriangle[i][j] = yhTriangle[i - 1][j - 1] + yhTriangle[i - 1][j];}}return yhTriangle[n][k];
}

        其实可以将二维数组转换为一维数组进行操作,效率会高点,并且和上面的代码几乎一样,实现起来也比较简单。这里本来可以用long数组来追求更大的适用范围,不过这样也比较耗内存。因此就放弃了此想法。

杨辉三角优化法

        上面操作中,开辟的杨辉三角二维数组利用率很低,并且我们处理的数据都是在两行数组中进行操作,与此同时,C(n, k)值的计算并不需要大于K部分的数值,因此我们可以选用两个K + 1长度的缓存数组来实现杨辉三角的操作。具体如下:

public static int nchoosek(int n, int k){if(n > 70 || (n == 70 && k > 25 && k < 45)){throw new IllegalArgumentException("N(" + n + ") and k(" + k + ") don't meet the requirements.");}checknk(n, k);k = (k > (n - k)) ? n - k : k;if(k <= 1){  // C(n, 0) = 1, C(n, 1) = nreturn k == 0 ? 1 : n;}int cacheLen = k + 1;long[] befores = new long[cacheLen];befores[0] = 1;long[] afters = new long[cacheLen];afters[0] = 1;for(int i = 1; i <= n; i++){for(int j = 1; j <= k; j++){afters[j] = befores[j - 1] + befores[j];}System.arraycopy(afters, 1, befores, 1, k);}return befores[k];
}

        使用极小的缓存来保存所需的信息,能极大提升空间利用率和程序执行效率。

二进制法

        打个比方,0b0000到0b1111,即从0到15,包含了4位中1的所有组合情况,如果我们想知道C(4, 2)的结果,只需要判断这十六个数的二进制值含2个1的数量。具体代码如下:

public static int nchoosek(int n, int k){if(n > 31){throw new IllegalArgumentException("N must be less than or equal to 31");}checknk(n, k);k = (k > (n - k)) ? n - k : k;if(k <= 1){  // C(n, 0) = 1, C(n, 1) = nreturn k == 0 ? 1 : n;}int limit = Integer.MAX_VALUE >> (31 - n);int cnk = 0;for(int i = 3; i < limit; i++){if(Integer.bitCount(i) == k){cnk++;}}return cnk;
}

        该方法的局限性就是判断的长度严格受整型长度限制,只能判断31以内的组合数,并且随着n越来越大,需要遍历的情况也是逐级递增。一般而言,用它获取17以内的组合数。   这句话主要依据来自于Matlab的代码,它使用该方法的判断条件就是17。

组合数计算小结

        上面的这些算法都只是单纯为了解决组合数计算的问题,至于n和k过大,所产生的大数的问题,并不在这篇博文的讨论范畴,大数比较复杂,三言两语说不清楚,详细说起来可以另写一篇博文,下次有机会再说(又挖了个坑)。Java里面处理大数一般使用BigInteger和BigDecimal。

        其实通过上面的相关算法我们可以看出,对于数学问题,数学公式和性质是提升算法性能的超级法宝。很多时候我们在语法层面的优化还敌不过一个简单的数学变换,因此对于数学方面的算法编写,最好是先了解数学方面的知识,再开始转为相应的算法,这样代码质量会更高点。

        通过上面的两种实现方式,可以得到三点启示:一是,以后遇到累积和累除问题,或者说遇到大量乘法和除法操作,一定要注意精度和溢出问题。二是,累积时要想到数字的相关性质,对于整数,素数有时会极大简化问题。三是,对于缓存或者说打表问题,一定要注意是否存在减小开辟空间的情况,并注意操作的特性,是否能简化操作,并不是一成不变,什么操作都执行,什么都信息都保存,一定要提升内存利用率。

获取数组的组合排列

        前面我们计算了全组合数,接下来我们来聊聊,如何获取数组指定数量的元素的排列组合。

二进制法

        前面我们使用二进制法来获取组合数,我们知道0b0000到0b1111这16个数中,二进制中含两个1的数都互不相同,因此我们可以根据二进制中1的排列位置,来间接获取元素的排列情况。具体代码如下:

public static List<int[]> nchoosek(int[] array, int k){int n = array.length;if(n > 31){throw new IllegalArgumentException("N must be less than or equal to 31");}checknk(n, k);if(k == 0){return new ArrayList<int[]>();}if(n == k){List<int[]> combList = new ArrayList<int[]>(1);combList.add(java.util.Arrays.copyOf(array, k));return combList;}int combNum = nchoosek(n, (k > (n - k)) ? n - k : k);int bits = Integer.MAX_VALUE >> (31 - n);List<int[]> combList = new ArrayList<int[]>(combNum);for(int i = 0; i < bits; i++){if(Integer.bitCount(i) != k){continue;}int[] comb = new int[k];int index = 0;int iTemp = i;while(iTemp != 0){int lowest = iTemp & -iTemp;comb[index++] = array[(int)(Math.log(lowest) / Math.log(2))];iTemp &= iTemp - 1;}combList.add(comb);}return combList;
}

        当n=31时,其所有全组合数之和为2147483647,刚好等于Integer.MAX_VALUE,如果我们直接遍历:0:2147483647,将会形成死循环。为了兼顾n = 31的特殊情况,我们就把所有n = k的情况单独拿出考虑,遍历范围变为0:2^{n} - 2

        这里面取二进制中1的具体位置思路为:先取二进制的最低位,该算法参见Integer.lowestOneBit,然后利用log_{2}x计算最低位的位置,最后利用x & (x - 1)将最低位及以后的位全部置零,这部分可以参考我的博文:Java计算二进制数中1的个数。

        其实这部分还可以使用逐位判1来进行操作,具体实现如下:

/******** 第一种 ********/
for(int j = 0; j < n; j++){if((i & (1 << j)) >= 1){comb[index++] = array[j];}
}
/******** 第二种 ********/
for(int j = 0; j < n; j++){if(i << (31 - j)) >>> 31 == 1){comb[index++] = array[j];}
}

基本迭代法

        下面是C(5, 3)的树状结构图,待排列的数组array = [1,2,3,4,5],数组长度n = 5,排列数组comb,它的长度k = 3。

        在开始操作前,我们记数组array的索引值为aIndex,数组comb的索引值为cIndex,第一个数(即cIndex = 0时)只能取1、2、3,因为当我们取4时,数组array不能提供足够的数值存放在comb中,因此它的限制值limit = n - (k - cIndex),k - cIndex表示填满数组comb所需的数据量。然后迭代,从第一层取到第三层,comb满了就保存在List<int[]>列表中。具体代码如下:

public static List<int[]> nchoosek(int[] array, int k){int n = array.length;checknk(n, k);List<int[]> combList = new ArrayList<>(nchoosek(n, k > (n - k) ? n - k : k));nchoosek0(array, n, new int[k], k, 0, 0, combList);return combList;
}private static void nchoosek0(int[] array, int n, int[] comb, int k,int aIndex, int cIndex, List<int[]> combList){if(cIndex == k){combList.add(Arrays.copyOf(comb, k));return;}for(int i = aIndex, limit = n - (k - cIndex); i <= limit; i++){comb[cIndex++] = array[i];nchoosek0(array, n, comb, k, i + 1, cIndex, combList);cIndex--;}
}

        其实上面中的cIndex == k的情况,可以直接放在for循环里面进行处理,经测试,两者并无效率差距。上面代码量比较小,只是迭代遍历取值罢了,它的思想和整数划分比较像,下次有机会写篇整数划分的文章(又挖了个坑)。

从后向前迭代法(Matlab版本)

        前面那个方法是从上向下进行迭代,其实我们也可以从下向上迭代。这里我以1的那个树状结构作为例子进行说明。

        后面的2和3都是采用这样的方式进行操作。具体代码如下:

public static int[][] nchoosek(int[] array, int k){int n = array.length;checknk(n, k);if(k == 0){return new int[1][0];}return nchoosek0(array, n, k);
}private static int[][] nchoosek0(int[] array, int n, int k){int[][] comb = null;if(n == k){comb = new int[1][n];for(int i = 0; i < n; i++){comb[0][i] = array[i];}return comb;}if(k == 1){comb = new int[n][1];for(int i = 0; i < n; i++){comb[i][0] = array[i];}return comb;} for(int i = 0, limit = n - k + 1; i < limit; i++){int[][] next = nchoosek0(Arrays.copyOfRange(array, i + 1, n), n - i - 1, k - 1); // Get all possible values for the next oneint combRowLen = comb == null ? 0 : comb.length;int totalRowLen = next.length + combRowLen;int totalColLen = next[0].length + 1;int[][] tempComb = new int[totalRowLen][totalColLen];if(comb != null){ // TempComb capacity expansion combfor(int j = 0; j < combRowLen; j++){tempComb[j] = Arrays.copyOf(comb[j], totalColLen);}}int value = array[i];for(int row = combRowLen; row < totalRowLen; row++){ tempComb[row][0] = value; // The value completes the current onefor(int col = 1; col < totalColLen; col++){ // Copy the next one.tempComb[row][col] = next[row - combRowLen][col - 1];}}comb = tempComb;}return comb;
}

        结合前面的图示来看这个代码,思路还是比较清晰的。这个代码网上并没有任何博文或者资料介绍过,这个算法纯粹是来自于Matlab(我的是2014a版本)的nchoosek函数下的combs函数,函数上注释了这么一句话:This function is only practical for situations where M is less than about 15。也就是说该函数最好是处理K<15的情况,因为K越大,迭代的次数越多,效率也就越低。

从后向前优化法

        我们把所有C(5, 3)的所以情况打印如下:

        在图上我们可以看出,红色框框部分,以2为首的矩阵,它的二三列数据都在1为首的矩阵中。对于紫色框框,3为首的数据,同样也包含在2为首的矩阵中。因此我们只需要把1为首的所有情况求出来,其余的都可以复制矩阵块即可,这大大提高了原先从后向前迭代法的效率。具体代码如下:

public static int[][] nchoosek(int[] array, int k){int n = array.length;checknk(n, k);if(k == 0){return new int[1][0];}int combNum = nchoosek(n, k > (n - k) ? n - k : k);int[][] comb = new int[combNum][k];int rowEndIndex = n - k + 1;for(int i = 0, k1 = k - 1; i < rowEndIndex; i++){ // Fill the right-most side.comb[i][k1] = array[k1 + i];}for(int begin = k - 2; begin >= 0; begin--){int rowLen = rowEndIndex;int previousRowEndIndex = rowEndIndex;for(int i = 0; i < rowEndIndex; i++){comb[i][begin] = array[begin];}for(int next = begin + 1, limit = begin + n - k; next <= limit; next++){ int selectionNum = n - k + 1 + begin - next;int allPossibleNum = n - next;rowLen = rowLen * selectionNum / allPossibleNum;int rowBeginIndex = rowEndIndex;rowEndIndex = rowBeginIndex + rowLen;int nextVal = array[next];for(int i = rowBeginIndex; i < rowEndIndex; i++){comb[i][begin] = nextVal;for(int j = begin + 1; j < k; j++){comb[i][j] = comb[previousRowEndIndex - rowLen + i - rowBeginIndex][j];}}}}return comb;
}

        这方法相比于原先迭代方法,不仅不需要扩容comb矩阵,而且无需迭代,并且没有重复运算,将第一个计算完了,后面都可以通过复制矩阵块来完成。这方法在Matlab上面运行效率比Matlab自带的库函数高,随着K值增大,两者差距越来越大。这个算法主要参见这篇文章:[代码提速实战] nchoosek的提速(28X)。因为Matlab对矩阵的下标操作和Java不同,因此修改将Matlab代码转换为Java代码时,一定要格外注意下标问题。

组合排列小结

        组合排列其实和整数划分和全排列一般,都是通过迭代进行操作,但是迭代就有缓存的可能,因此以后遇到迭代问题,一定要考虑缓存问题,以此来优化算法。

后记

        总算写完,哎,手的敲疼了,其实也不知道能有多少人坚持看到这里。算法的学习毕竟是枯燥,希望自己能坚持到底。又是十二点多了,下次要注意了。不知道有没有时间把整数划分和全排列补上,看心情吧,哈哈哈!


http://chatgpt.dhexx.cn/article/PplTFTY3.shtml

相关文章

[组合] 组合数计算四大算法模板(模板+卢卡斯定理)

文章目录 0. 前言1. 预处理组合数组合递推式2. 预处理阶乘逆元3. 卢卡斯定理4. 高精度组合数 0. 前言 组合数求解有很多种方式&#xff0c;不同的方式对应这不同的时间复杂度&#xff0c;难以程度也是不尽相同。根据数据范围选择对应的方法即可。 1. 预处理组合数组合递推式 …

组合数

1、定义&#xff1a;从m个不同元素中&#xff0c;任取n(n≤m)个元素并成一组&#xff0c;叫做从m个不同元素中取出n个元素的一个组合&#xff1b;从m个不同元素中取出n(n≤m)个元素的所有组合的个数&#xff0c;叫做从m个不同元素中取出n个元素的组合数。 2、公式&#xff1a;…

组合数取模总结

题目描述&#xff1a; 在ACM竞赛现在叫JB竞赛&#xff1f;中&#xff0c;经常会遇到组合数取模的题目&#xff1b;就我现在的水平而言&#xff0c;大概分为以下三类&#xff0c;以后遇到新的方法会在做补充&#xff1b; 第一种&#xff1a; n和m都较小 &#xff08;<1000&a…

逆元求组合数

逆元简介 同余符号 ≡ 先bb一下 ≡&#xff0c;这个符号有三个意思&#xff0c;再这里用到的意思为“同余符号”。≡ 的介绍 两个整数a&#xff0c;b&#xff0c;若它们除以整数m所得的余数相等&#xff0c;则称a&#xff0c;b对于模m同余 记作a≡b(mod m) 读作a同余于b模m&am…

组合数(Combinatorial_Number)

定义&#xff1a; 从n个不同元素中&#xff0c;任取m(m≤n)个元素并成一组&#xff0c;叫做从n个不同元素中取出m个元素的一个组合;从n个不同元素中取出m(m≤n)个元素的所有组合的个数&#xff0c;叫做从n个不同元素中取出m个元素的组合数。 公式&#xff1a; 在线性写法中被…

组合数的计算

本篇博客来自南昌理工学院acm集训队成员yyj 组合数 1.定义 组合数&#xff1a;从 n 个不同元素中每次取出 m 个不同元素 &#xff0c;不管其顺序合成一组&#xff0c;称为从 n 个元素中不重复地选取 m 个元素的一个组合。所有这样的组合的种数称为组合数。 2.性质与描述 2…

组合数的性质证明

性质 极其简单的证明 根据杨辉三角的性质&#xff0c;第x行第y个的值为C(x-1,y-1) 那么 一目了然。 严肃的证明

组合数性质--二项式系数之和等于2^n的证明

1.公式 首先我们都知道组合数的意义&#xff0c;就是说一共有n个样本&#xff0c;一次性从中取出m个样本&#xff0c;一共有多少种不同的取法。它的公式如下&#xff1a; 它有这么一个性质&#xff1a; 该性质有若干种证明方式&#xff0c;今天我在这边写出我觉得挺巧妙的一种…

组合数原理

资料来源&#xff1a;https://baike.baidu.com/ 组合数 从n个不同元素中&#xff0c;任取m(m≤n)个元素并成一组&#xff0c;叫做从n个不同元素中取出m个元素的一个组合&#xff1b;从n个不同元素中取出m(m≤n)个元素的所有组合的个数&#xff0c;叫做从n个不同元素中取出m个…

深入详细理解矩阵 (矩阵的加减乘、转置、共轭、共轭转置)

简介 矩阵:英文名Matrix。在数学名词中&#xff0c;矩阵用来表示统计数据等方面的各种有关联的数据。这个定义很好地解释了Matrix代码制造世界的数学逻辑基础。矩阵是数学中最重要的基本概念之一&#xff0c;是代数学的一个主要研究对象&#xff0c;也是数学研究及应用的一个重…

【线性代数之二】矩阵与行列式

一、矩阵 1.1 定义 由 m n 个数aij排成的m行n列的数表称为m行n列的矩阵&#xff0c;简称m n矩阵。记作&#xff1a; 这mn 个数称为矩阵A的元素&#xff0c;简称为元&#xff0c;数aij位于矩阵A的第i行第j列&#xff0c;称为矩阵A的(i,j)元&#xff0c;以数 aij为(i,j)元的…

CString类型转换为LPCSTR类型

今天编程遇到一个问题&#xff0c;就是openGL中某个函数需要传入LPCSTR类型的参数&#xff0c;而通过MFC对话框获取得到的是CString类型的参数&#xff0c;因此需要将CString转化为LPCSTR类型&#xff0c;网上有很多这样的强转类型&#xff0c;然而却发现在强转的时候没有用&am…

linux jstat 简介

本文目录一览&#xff1a; 1、Linux使用jstat命令查看jvm的GC情况2、linux怎么监控 jvm内存 jstat3、Linux系统监控要用到哪些命令4、linux上如何安装jstatd服务 Linux使用jstat命令查看jvm的GC情况 Linux 使用jstat命令查看jvm的GC情况 命令格式 jstat命令命令格式&#…

内存屏障与volatile(C语言版)

内存屏障与volatile&#xff08;C语言版&#xff09; 最有价值的写在最前面 内存屏障与volatile是高并发编程中比较常用的两个技术&#xff0c;无锁队列的时候就会用到这两项技术。然而这两项技术设计比较广的基础知识&#xff0c;所以比较难以理解&#xff0c;也比较不容易解…

WDM驱动

WDM英文Windows Driver Model(WDM)的缩写。 简介 WDM WDM是WINDOWS2000认证的驱动程序&#xff0c;WIN2000由NT发展而来&#xff0c;所以对于设备的支持功能有限&#xff0c;同时为了最大限度的保障稳定性&#xff0c;所以推崇WDM驱动&#xff0c;但同时WDM驱动也就是功能最少…

快速转换dBm与W

dBm是一个表示功率绝对值的值&#xff08;也可以认为是以1mW功率为基准的一个比值&#xff09;&#xff0c;计算公式为&#xff1a;10log&#xff08;功率值/1mw&#xff09;。 这里我们介绍一种将dBm转换为W的口算方法&#xff0c;这一方法总结起来就是 “1个基准”和“2个原则…

学内核之十一:ARM64屏障指令使用指南

关于屏障指令&#xff0c;主要是与乱序有关。特别是在内核开发中&#xff0c;这是一个非常重要的主题。屏障由乱序引起&#xff0c;乱序则是由优化引起。 自从摩尔定律不断被逼近极限&#xff0c;半导体的优化不再单纯通过提升频率来实现。多核心、并发执行变成了主流的优化思路…