Uploaded image for project: 'Commons Numbers'
  1. Commons Numbers
  2. NUMBERS-183

Improve binomial coefficient classes

    XMLWordPrintableJSON

Details

    • Improvement
    • Status: Closed
    • Minor
    • Resolution: Implemented
    • 1.0
    • 1.1
    • combinatorics
    • None

    Description

      The binomial coefficient classes can be improved by several small changes.

      Avoid recursion

      All classes use a recursive method call to compute either C(n, k) or C(n, n - k) depending which is smaller. The methods can avoid recursion using m = min(k, n - k) and computing C(n, m).

      Threshold for overflow

      The LogBinomialCoeffiecient knows that C(1030, 515) overflows a double. But the BinomialCoefficientDouble does not. It will compute C(100000, 50000) with a loop of 50000 computations and return infinity. The class should be updated with some better thresholds:

      C(1030, 515)           ~ 2.85e308   =>  m >= 515  is overflow
      C(2147483647, 37) * 37 ~ 5.13e303   =>  m < 38    cannot overflow
      

      Overflow protection

      Better overflow protection is required for large results. These all currently fail as they overflow to infinity in the intermediate computation:

          @ParameterizedTest
          @CsvSource({
              "1040, 450, 2.3101613255412135615e307",
              "1029, 514, 1.4298206864989040819e308",
              "1786388282, 38, 7.187239013254065384599502085053593e306",
              "1914878305, 38, 100.6570419073661447979173868523364e306",
              "1179067476, 39, 30.22890249420109200962786203300876e306",
              "2147483647, 37, 1.388890512412231479281222156415993e302",
          })
          void testBinomialCoefficientLargeK(int n, int k, double nCk) {
              final double x = BinomialCoefficientDouble.value(n, k);
              Assertions.assertEquals(nCk, x, Math.ulp(nCk) * 10,
                  () -> "ULP error: " + (x - nCk) / Math.ulp(nCk));
          }
      

      A test for overflow in the loop can rearrange the terms to compute the correct result.

      Use the known factorials

      The BinomialCoefficientDouble class can exploit the known factorials stored in the Factorial class and avoid a loop computation for small n:

      if (n <= 170) {
          // Use fast table lookup:
          result = Factorial.doubleValue(n);
          // Smaller m will have a more accurate factorial and may be exact
          result /= Factorial.doubleValue(m);
          result /= Factorial.doubleValue(n - m);
      }

      The result appears to be as accurate as the current implementation on a partial test dataset. The complete dataset of factorials will require approximately (sum i=1 ... 170/2) * 2 == 7310 values so could be tested to see if this fast method is robust.

      Avoid cancellation in summations

      The LogBinomialCoefficient sum will suffer from cancellation:

              // Sum for values that could overflow.
              double logSum = 0;
      
              // n! / (n - k)!
              for (int i = n - k + 1; i <= n; i++) {
                  logSum += Math.log(i);
              }
      
              // Divide by k!
              for (int i = 2; i <= k; i++) {
                  logSum -= Math.log(i);
              }
      
              return logSum;
      

      The divide by k! should be summed separately and subtracted as a single sum. This could be performed using the LogFactorial class which can cache results for reuse.

      Use the gamma functions

      Note: The combinatorics package depends on the gamma package. The factorial can be computed using the gamma function:

      n! = gamma(n+1)
      

      This is used in the LogFactorial class which delegates to LogGamma.

      The binomial coefficient can be computed using the beta function:

         n!                gamma(n+1)               gamma(k+1) * gamma(n-k+1)
      ---------   = ------------------------- = 1 / -------------------------
      k! (n-k)!     gamma(k+1) * gamma(n-k+1)              gamma(n+1)
      
      
                  = 1 / (k * beta(k, n-k+1)) = 1 / ((n-k) * beta(k+1, n-k))
      
      
      beta(a, b) = gamma(a) * gamma(b) / gamma(a+b)
      

      Note: In converting the Boost beta function implementation (NUMBERS-181) I have tested using the beta function to compute the binomial coefficient. It is not more accurate than using the current implementation within a loop to compute the multiplication of terms.

      I have not tested using LogBeta to compute the LogBinomialCoefficient. The accuracy of this should be checked against using the current summation of logs within a loop.

       

      Attachments

        Activity

          People

            Unassigned Unassigned
            aherbert Alex Herbert
            Votes:
            0 Vote for this issue
            Watchers:
            1 Start watching this issue

            Dates

              Created:
              Updated:
              Resolved: