【问题标题】:Combinatory issue due to Factorial overflow由于阶乘溢出导致的组合问题
【发布时间】:2015-04-17 23:25:00
【问题描述】:

我需要一个函数来计算纸牌游戏的 (n, k) 的mathematical combination

我目前的尝试是使用基于通常 Factorial 方法的函数:

    static long Factorial(long n)
    {
        return n < 2 ? 1 : n * Factorial(n - 1); 
    }

    static long Combinatory(long n , long k )
    {
        return Factorial(n) / (Factorial(k) * Factorial(n - k)); 
    }

它工作得很好,但问题是当我使用某个数字范围时(n 值最大值为 52,k 值最大值为 4),它让我返回一个错误的值。例如:

   long comb = Combinatory(52, 2) ; // return 1 which should be actually 1326

我知道这是因为我在做 Factorial(52) 时溢出了 long 但我需要的范围结果并不像看起来那么大。

有什么办法可以解决这个问题吗?

【问题讨论】:

  • 您正在寻找计算二项式。 stackoverflow.com/questions/1495856/…
  • 你为什么用 C# 和 Java 标记这个?
  • @p.s.w.g : 因为这两个代码都是用 Java 和 C# 编译的,而且 long 都是 64 位的。
  • 我很好奇你为什么需要计算这么大的数字。一个近似值就足够了吗?

标签: java c# math


【解决方案1】:

不要使用默认的组合公式n! / (k! x (n - k)!),而是使用组合函数的递归属性。

    (n, k) = (n - 1, k) + (n - 1, k - 1)

知道:(n, 0) = 1(n, n) = 1

->它会让你避免使用阶乘和溢出你的long。

这是您可以执行的示例:

 static long Combinatory(long n, long k)
    {
        if (k == 0 || n == k )
            return 1;

        return Combinatory(n - 1, k) + Combinatory(n - 1, k - 1); 
    }

编辑:使用更快的迭代算法

    static long Combinatory(long n, long k)
    {
        if (n - k < k)
            k = n - k;

        long res = 1;

        for (int i = 1; i <= k; ++i)
        {
            res = (res * (n - i + 1)) / i;
        }

        return res;
    } 

【讨论】:

  • 超级效率低下,而且根据他所说的数字,他可能仍然无法实际计算出他需要的值。这就像如何不使用递归的教科书示例。
  • @Servy,也是一个教科书示例,说明何时使用 memoization。也可以利用身份 (n, k) = (n, n-k)
  • 是的,因为 4!仍然不是那么大的数字,但是一旦达到大约 8,就会达到需要很长时间的地步,如果您可以等待相当长的时间,则只会过去几个。这就是使用 O(N!) 算法的问题。
  • @jameslarge 是的,这是一种可能的方法,它不会让这个性能变得非常糟糕。
  • @Servy :我对答案进行了编辑。希望能让你满意
【解决方案2】:

在 C# 中,您可以使用 BigInteger(我认为有一个 Java 等价物)。

例如:

static long Combinatory(long n, long k)
{
    return (long)(Factorial(new BigInteger(n)) / (Factorial(new BigInteger(k)) * Factorial(new BigInteger(n - k))));
}

static BigInteger Factorial(BigInteger n)
{
    return n < 2 ? 1 : n * Factorial(n - 1);
}

您需要添加对System.Numerics 的引用才能使用 BigInteger。

【讨论】:

  • 谢谢你的回答
  • Java 有一个 BigInteger 类,但它没有运算符重载,因此您需要调整 /&lt;*-
【解决方案3】:

如果这不是家庭作业,Apache 的 commons-math 包中有一个高效的实现

http://commons.apache.org/proper/commons-math/apidocs/org/apache/commons/math3/util/ArithmeticUtils.html#binomialCoefficientDouble%28int,%20int%29

如果是为了家庭作业,开始在你的实施中避免阶乘。

使用 (n, k) = (n, n-k) 的属性,使用 k 的最大值重写您的选择。

然后注意你可以减少 n!/k!(n-k)!到 n * n-1 * n-2 .... * k / (n-k) * (n-k-1) ... * 1 表示您将 [k, n] 包括在内的每个数字相乘,然后除以每个数字 [1,n-k] 包括在内。

// From memory, please verify correctness independently before trusting its use.
//
public long choose(n, k) {
  long kPrime = Math.max(k, n-k);
  long returnValue = 1;
  for(i = kPrime; i <= n; i++) {
    returnValue *= i;
  }
  for(i = 2; i <= n - kPrime; i++) {
    returnValue /= i;
  }
  return returnValue;
}

请仔细检查数学,但这是一个基本想法,您可以考虑得到一个相当有效的实现,该实现适用于扑克牌的数字。

【讨论】:

    【解决方案4】:

    递归公式也称为Pascal's triangle,IMO 是计算组合的最简单方法。如果您只需要 C(52,k) (对于 0

    static  int64_t*    pascals_triangle( int N)
    {
    int n,k;
    int64_t*    C = calloc( N+1, sizeof *C);
        for( n=0; n<=N; ++n)
        {   C[n] = 1;
            for( k=n-1; k>0; --k)
            {   C[k] += C[k-1];
            }
        }
        return C;
    }
    

    在使用 N=52 调用此函数后,例如返回,C[k] 将保持 C(52,k) 为 k=0..52

    【讨论】:

      猜你喜欢
      • 2023-03-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多