【问题标题】:Combinatoric 'N choose R' in java math?java数学中的组合'N选择R'?
【发布时间】:2010-02-04 16:03:08
【问题描述】:

Java 库中是否有内置方法可以为任意 N、R 计算“N 选择 R”?

【问题讨论】:

  • 如果结果溢出一个int怎么办?这有关系吗?你想要一个 BigInteger 的结果吗?
  • 我只是想计算不同牌组大小的 2 张卡片组合的数量(最多 52 个),所以不应该超过 1,326(52 个选择 2)
  • 你知道52!是80658175170943878571660636856403766975289505440883277824000000000000?因为从您接受的答案来看,您似乎没有考虑过该公式中涉及的数字的大小。顺便说一句,两张牌的答案是 (n*(n-1))/2。您不需要完整实现 'n choose r' 来获得它。
  • 请参阅 stackoverflow.com/questions/1678690/… 了解实施注意事项。
  • 哦,好的,谢谢你是对的,我有整数溢出

标签: java math combinatorics


【解决方案1】:

公式

实际上计算N choose K 非常容易,甚至不需要计算阶乘。

我们知道(N choose K) 的公式是:

    N!
 --------
 (N-K)!K!

因此,(N choose K+1) 的公式为:

       N!                N!                   N!               N!      (N-K)
---------------- = --------------- = -------------------- = -------- x -----
(N-(K+1))!(K+1)!   (N-K-1)! (K+1)!   (N-K)!/(N-K) K!(K+1)   (N-K)!K!   (K+1)

即:

(N choose K+1) = (N choose K) * (N-K)/(K+1)

我们也知道(N choose 0)是:

 N!
---- = 1
N!0!

所以这为我们提供了一个简单的起点,并且使用上面的公式,我们可以找到 (N choose K)K 乘法和 K 除法的任何 K > 0


简单的帕斯卡三角

综上所述,我们可以很容易地生成帕斯卡三角形如下:

    for (int n = 0; n < 10; n++) {
        int nCk = 1;
        for (int k = 0; k <= n; k++) {
            System.out.print(nCk + " ");
            nCk = nCk * (n-k) / (k+1);
        }
        System.out.println();
    }

打印出来:

1 
1 1 
1 2 1 
1 3 3 1 
1 4 6 4 1 
1 5 10 10 5 1 
1 6 15 20 15 6 1 
1 7 21 35 35 21 7 1 
1 8 28 56 70 56 28 8 1 
1 9 36 84 126 126 84 36 9 1 

BigInteger版本

应用BigInteger 的公式很简单:

static BigInteger binomial(final int N, final int K) {
    BigInteger ret = BigInteger.ONE;
    for (int k = 0; k < K; k++) {
        ret = ret.multiply(BigInteger.valueOf(N-k))
                 .divide(BigInteger.valueOf(k+1));
    }
    return ret;
}

//...
System.out.println(binomial(133, 71));
// prints "555687036928510235891585199545206017600"

根据谷歌,133 choose 71 = 5.55687037 × 1038


参考文献

【讨论】:

  • 我注意到使用这种方法(与递归计算相比)有很大的性能提升
  • 您打印帕斯卡三角的解决方案不适用于大数。以 30 为例。
  • 关于二项式函数的一点实现说明:您可以迭代到 min(N-K, K),而不是迭代到 K。
  • 很好的解释!。我们如何证明 (n-k)/(k+1) * nCk 永远不会导致浮点数?
  • 很好的解释。非常感谢。
【解决方案2】:

apache-commons“数学”支持这一点 org.apache.commons.math4.util.CombinatoricsUtils

【讨论】:

【解决方案3】:

recursive definition 为您提供了一个非常简单的选择功能,该功能适用​​于较小的值。如果您打算大量运行此方法,或者在较大的值上运行此方法,那么记住它会很划算,但除此之外就可以了。

public static long choose(long total, long choose){
    if(total < choose)
        return 0;
    if(choose == 0 || choose == total)
        return 1;
    return choose(total-1,choose-1)+choose(total-1,choose);
}

改进此函数的运行时保留为exercise for the reader :)

【讨论】:

  • 我讨厌吹毛求疵,但我很好奇为什么我刚刚对这个答案投了反对票。当然polygenelubricants的答案要详细得多,但是我已经多次使用此功能没有问题,是否有某种错误?
  • 我没有对你投反对票,但你的算法会调用选择 2*C(n,k)-1 次。 IE。在计算 choose(10,5) 时,它将进行 503 次递归调用 (2*C(10,5)-1 = 2*252-1 = 504-1)。所以它没有希望计算大约 1380 亿的 C(40,20)。
  • 同意,基本的 Fibonacci 实现或任何数量的受益于动态编程的算法也是如此。您是否认为我的回答(特别是记忆方法的建议)不足以涵盖这一点?
  • 也许如果您编写的代码包含记忆功能,您就不会被否决。就目前而言,它不是很有用。但是有些人认为您的回答有所贡献,因为您确实获得了一些支持。
  • 是的,也许。就个人而言,动态编程与实际问题是正交的,不同的应用程序需求将需要不同的缓存方案,但我承认有人可能会因为不能按原样放弃这个答案而感到不满。无论如何,我会尽量不要为此失眠:)
【解决方案4】:

我只是想计算不同牌组大小的 2 张卡片组合的数量...

无需导入外部库 - 从组合的定义中,与 n 卡将是 n*(n-1)/2

额外问题: 同样的公式计算第一个 n-1 整数的总和 - 你明白为什么它们是相同的吗? :)

【讨论】:

  • (对奖金问题的回答,一年后:有n-1 方法将第一张卡与另一张卡配对,还有n-2 方法将第二张卡与其余卡配对等)
【解决方案5】:

N!/((R!)(N-R)!)

在这个公式中你可以取消很多,所以通常阶乘是没有问题的。假设 R > (N-R) 然后取消 N!/R!到 (R+1) * (R+2) * ... * N。但确实,int 非常有限(大约 13!)。

但是每次迭代也可以划分。在伪代码中:

d := 1
r := 1

m := max(R, N-R)+1
for (; m <= N; m++, d++ ) {
    r *= m
    r /= d
}

从一个开始除法很重要,尽管这似乎是多余的。但是让我们举个例子:

for N = 6, R = 2: 6!/(2!*4!) => 5*6/(1*2)

如果我们省略 1,我们将计算 5/2*6。除法将离开整数域。保留 1 我们保证不会这样做,因为乘法的第一个或第二个操作数都是偶数。

出于同样的原因,我们不使用r *= (m/d)

整个事情可以修改为

r := max(R, N-R)+1
for (m := r+1,d := 2; m <= N; m++, d++ ) {
    r *= m
    r /= d
}

【讨论】:

    【解决方案6】:

    binomialCoefficient,在Commons Math

    返回二项式系数的精确表示,“n 选择 k”,即可以从 n 元素集中选择的 k 元素子集的数量。

    【讨论】:

      【解决方案7】:

      数学公式是:

      N!/((R!)(N-R)!)
      

      应该不难从那里弄清楚:)

      【讨论】:

      • 是的,应该。例如,如果您受限于 int 的大小,您就不想计算巨大的阶乘。
      • @jleedev:好像首先考虑到这一点就那么难:P
      • 你可以做一些分解来清理它,但这个定义仍然比你想要使用的数字大得多。为了获得最佳结果,选择算法永远不应该计算比结果更大的数字 - 这些数字会很快变得足够大。
      • @dimo414。如果你按照书面方式实现它,当然每次都会溢出。如果从较大的 R 中删除所有因素!和 (N-R)!,然后使用结果始终为整数的事实,您无需担心中间结果溢出。
      【解决方案8】:

      以下例程将使用递归定义和记忆来计算 n-choose-k。该例程非常快速且准确:

      inline unsigned long long n_choose_k(const unsigned long long& n,
                                           const unsigned long long& k)
      {
         if (n  < k) return 0;
         if (0 == n) return 0;
         if (0 == k) return 1;
         if (n == k) return 1;
         if (1 == k) return n;
         typedef unsigned long long value_type;
         value_type* table = new value_type[static_cast<std::size_t>(n * n)];
         std::fill_n(table,n * n,0);
         class n_choose_k_impl
         {
         public:
      
            n_choose_k_impl(value_type* table,const value_type& dimension)
            : table_(table),
              dimension_(dimension)
            {}
      
            inline value_type& lookup(const value_type& n, const value_type& k)
            {
               return table_[dimension_ * n + k];
            }
      
            inline value_type compute(const value_type& n, const value_type& k)
            {
               if ((0 == k) || (k == n))
                  return 1;
               value_type v1 = lookup(n - 1,k - 1);
               if (0 == v1)
                  v1 = lookup(n - 1,k - 1) = compute(n - 1,k - 1);
               value_type v2 = lookup(n - 1,k);
               if (0 == v2)
                  v2 = lookup(n - 1,k) = compute(n - 1,k);
               return v1 + v2;
            }
      
            value_type* table_;
            value_type dimension_;
         };
         value_type result = n_choose_k_impl(table,n).compute(n,k);
         delete [] table;
         return result;
      }
      

      【讨论】:

      • 这是 C++ 而不是 Java
      • @phw。转换为 java 所涉及的工作量很小。
      • 我不认为 java 有内联函数。可以去掉inline关键字对吗?
      【解决方案9】:
      【解决方案10】:

      ArithmeticUtils.factorial 现在显然已被弃用。请尝试CombinatoricsUtils.binomialCoefficientDouble(n,r)

      【讨论】:

        【解决方案11】:

        与 guava 版本类似,Richard J. Mathar 有一个 BigIntegerMath 类 here 称为 org.nevec.rjm,它是类的包。

        他们的实现为二项式方法提供了两个签名:int,int 和 BigInteger,BigInteger。

        【讨论】:

          【解决方案12】:

          使用 hashmap 改进 @dimo414 的解决方案:

          private static Map<Integer, Map<Integer, Integer>> map = new HashMap<>();
          private static int choose(int total, int choose){
              if(total < choose)
                  return 0;
              if(choose == 0 || choose == total)
                  return 1;
          
              if (! (map.containsKey(total) && map.get(total).containsKey(choose))){
                  map.put(total, new HashMap<>());
                  map.get(total).put(choose, choose(total-1,choose-1)+choose(total-1,choose));
              }
              return map.get(total).get(choose);
          }
          

          【讨论】:

            【解决方案13】:
            public static void combinationNcK(List<String> inputList, String prefix, int chooseCount, List<String> resultList) {
                if (chooseCount == 0)
                    resultList.add(prefix);
                else {
                    for (int i = 0; i < inputList.size(); i++)
                        combinationNcK(inputList.subList(i + 1, inputList.size()), prefix + "," + inputList.get(i), chooseCount - 1, resultList);
            
                    // Finally print once all combinations are done
                    if(prefix.equalsIgnoreCase("")){
                        resultList.stream().map(str->str.substring(1)).forEach(System.out::println);
                    }
                }
            }
            
            public static void main(String[] args) {
                List<String> positions = Arrays.asList(new String[] { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12" });
                List<String> resultList = new ArrayList<String>();
                combinationNcK(positions, "", 3, resultList);
            }
            

            【讨论】:

              【解决方案14】:

              根据公式:n!/ ((n-k)! * k!) 如果我们简单地计算分子和分母,很多计算将被浪费,并且可能“int”、“float”甚至“BigInteger”的范围可以填满。 因此,为了克服这种情况,我们甚至可以在乘以值之前将其抵消。

              假设 n=6,k=3

              即 => 6*5*4*3*2*1 / ((3*2) * (3*2))

              假设如果我们乘以分子,范围可以填满。更好的选择是在乘以值之前将其取消。

              在这种情况下--> 如果我们取消剩下的所有内容: (2*5*2)

              将这些值相乘要容易得多,并且需要更少的计算。

              ================================================ =======

              下面提到的代码将“有效地”用于以下数字:

              1. n == k
              2. k
              3. k == 0
              4. n 和 k 之间的差异太大,例如。 n=1000, k=2
              5. k = n/2(最艰难)
              6. k 值接近一半 n的值

              可能代码还可以改进。

              BigInteger calculateCombination(int num, int k) {
              
                  if (num == k || k == 0)
                      return BigInteger.ONE ;
              
                  int numMinusK = num - k;
                  int stopAt; // if n=100, k=2 , can stop the multiplication process at 100*99
                  int denominator;
              
                  // if n=100, k=98 OR n=100, k=2 --> output remains same.
                  // thus choosing the smaller number to multiply with
                  if (numMinusK > k) {
                      stopAt = numMinusK;
                      denominator = k;
                  } else {
                      stopAt = k;
                      denominator = numMinusK;
                  }
              
                  // adding all the denominator nums into list
                  List<Integer> denoFactList = new ArrayList<Integer>();
                  for (int i = 2; i <= denominator; i++) {
                      denoFactList.add(i);
                  }
              
                  // creating multiples list, because 42 / 27 is not possible
                  // but 42 / 3 and followed by 42 / 2 is also possible
                  // leaving us only with "7"
                  List<Integer> multiplesList = breakInMultiples(denoFactList);
                  Collections.sort(multiplesList, Collections.reverseOrder());
              
                  Iterator<Integer> itr;
                  BigInteger total = BigInteger.ONE;
                  while (num > 0 && num > stopAt) {
              
                      long numToMultiplyWith = num;
                      if (!multiplesList.isEmpty()) {
                          itr = multiplesList.iterator();
                          while (itr.hasNext()) {
                              int val = itr.next();
                              if (numToMultiplyWith % val == 0) {
                                  numToMultiplyWith = numToMultiplyWith / val;
                                  itr.remove();
                              }
                          }
                      }
              
                      total = total.multiply(BigInteger.valueOf(numToMultiplyWith));
                      num--;
                  }
                  return total;
              
              }
              
              ArrayList<Integer> breakInMultiples(List<Integer> denoFactList) {
                  ArrayList<Integer> multiplesList = new ArrayList<>();
                  for (int i : denoFactList)
                      updateListWithMultiplesOf(multiplesList, i);
                  return multiplesList;
              }
              
              void updateListWithMultiplesOf(ArrayList<Integer> list, int i) {
                  int count = 2;
                  while (i > 1) {
                      while (i % count == 0) {
                          list.add(count);
                          i = i / count;
                      }
                      count++;
                  }
              }
              

              【讨论】:

                【解决方案15】:

                已经提交了很多解决方案。

                1. 一些解决方案没有考虑整数溢出。

                2. 在给定 n 和 r 的情况下,某些解决方案会计算所有可能的 nCr。 结果是需要更多的时间和空间。

                在大多数情况下,我们需要直接计算 nCr。我将再分享一个解决方案。

                static long gcd(long a, long b) {
                    if (a == 0) return b;
                    return gcd(b%a, a);
                }
                
                // Compute (a^n) % m
                static long bigMod(long a, long n, long m) {
                    if (n == 0) return 1;
                    if (n == 1) return a % m;
                    long ret = bigMod(a, n/2, m);
                    ret = (ret * ret) % m;
                    if (n % 2 == 1) return (ret * a) % m;
                    return ret;
                }
                
                // Function to find (1/a mod m).
                // This function can find mod inverse if m are prime
                static long modInverseFarmetsTheorem(long a, long m) {
                    if (gcd(a, m) != 1) return -1;
                
                    return bigMod(a, m-2, m);
                }
                
                // This function finds ncr using modular multiplicative inverse
                static long ncr(long n, long r, long m) {
                    if (n == r) return 1;
                    if (r == 1) return n;
                
                    long start = n - Math.max(r, n - r) + 1;
                
                    long ret = 1;
                    for (long i = start; i <= n; i++) ret = (ret * i) % m;
                
                    long until = Math.min(r, n - r), denom = 1;
                    for (long i = 1; i <= until; i++) denom = (denom * i)  % m;
                
                    ret = (ret * modInverseFarmetsTheorem(denom, m)) % m;
                
                    return ret;
                }
                

                【讨论】:

                  【解决方案16】:

                  我们可以利用以下事实,而不是递归地实现 n 选择 k(这可能会变慢):

                                  n(n-1)(n-2)...(n-k+1)
                    n choose k =  --------------------
                                          k!
                  

                  我们仍然需要计算 k!,但这可以比递归方法快得多。

                  private static long choose(long n, long k) {
                      long numerator = 1;
                      long denominator = 1;
                  
                      for (long i = n; i >= (n - k + 1); i--) {
                          numerator *= i;
                      }
                  
                      for (long i = k; i >= 1; i--) {
                          denominator *= i;
                      }
                  
                      return (numerator / denominator);
                  }
                  

                  请注意,上面的选择方法假定 n 和 k 都不是负数。此外,对于足够大的值,长数据类型可能会溢出。如果结果相应,则应使用 BigInteger 版本。分子和/或分母预计超过 64 位。

                  【讨论】:

                    【解决方案17】:
                    public static long nCr(int n, int r) {
                        long a = n;
                        long b = r;
                        long c = (n - r);
                    
                        for (int o = (int)a - 1; o > 0; o--) { a = a * o; }
                        for (int o = (int)b - 1; o > 0; o--) { b = b * o; }
                        for (int o = (int)c - 1; o > 0; o--) { c = c * o; }
                    
                        return (a / (b * c)); // n! / r! * (n - r)!
                    }
                    

                    根据我几年前的回答编辑,其中 a、b 和 c 是整数,整数溢出使该方法严重无法使用。就可靠性而言,这个并没有更好,但它很懒。

                    如果值超过 long 的限制,这也会变砖...这不太可行,除非您试图为学校项目或其他东西找到一些快速解决方案。

                    【讨论】:

                    • 它无处不在。此外,这不会处理整数溢出。
                    • 非常懒惰地修复它;将阅读所写的其他答案。 c-- 飞过我的脑袋,我已经习惯了甚至不考虑整数溢出(因为那时我并没有真正做任何其他事情,这是一个问题)。谢谢你告诉我。
                    • 很抱歉,这个实现没有考虑整数溢出,这是计算nCr时的主要问题。
                    猜你喜欢
                    • 1970-01-01
                    • 2022-01-17
                    • 1970-01-01
                    • 2015-03-17
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2018-10-10
                    相关资源
                    最近更新 更多