计算组合的数量

时间:2009-12-03 08:03:43

标签: c++ algorithm combinatorics

干杯,

我知道您可以使用以下公式获得组合数量(不重复,顺序并不重要):

// Choose r from n

n! / r!(n - r)!

但是,我不知道如何在C ++中实现它,因为例如

n = 52

n! = 8,0658175170943878571660636856404e+67

即使对于unsigned __int64(或unsigned long long),数字也会变得太大。是否有一些解决方法来实现公式而没有任何第三方“bigint”-libraries?

10 个答案:

答案 0 :(得分:39)

这是一个古老的算法,它是精确的并且不会溢出,除非long long的结果很大

unsigned long long
choose(unsigned long long n, unsigned long long k) {
    if (k > n) {
        return 0;
    }
    unsigned long long r = 1;
    for (unsigned long long d = 1; d <= k; ++d) {
        r *= n--;
        r /= d;
    }
    return r;
}

这个算法也在Knuth的“计算机编程艺术,第3版,第2卷:研究数学算法”中。我想。

更新:算法在线路上溢出的可能性很小:

r *= n--;

非常大n。天真的上限是sqrt(std::numeric_limits<long long>::max()),这意味着n小于4,000,000,000。

答案 1 :(得分:27)

来自Andreas' answer

  

这是一个古老的算法,它是精确的并且不会溢出,除非long long的结果很大

unsigned long long
choose(unsigned long long n, unsigned long long k) {
    if (k > n) {
        return 0;
    }
    unsigned long long r = 1;
    for (unsigned long long d = 1; d <= k; ++d) {
        r *= n--;
        r /= d;
    }
    return r;
}
     

这个算法也在Knuth的“计算机编程艺术,第3版,第2卷:研究数学算法”中。我想。

     

更新:算法在线路上溢出的可能性很小:

r *= n--;
     

非常大n。天真的上限是sqrt(std::numeric_limits<long long>::max()),这意味着n小于4,000,000,000。

考虑n == 67和k == 33.上述算法溢出64位无符号长long。然而,正确答案可用64位表示:14,226,520,737,620,288,370。并且上面的算法没有提到它的溢出,选择(67,33)返回:

8,829,174,638,479,413

一个可信但不正确的答案。

然而,只要最终答案可以表示,上述算法可以稍微修改为永不溢出。

诀窍在于认识到在每次迭代时,除法r / d都是精确的。暂时改写:

r = r * n / d;
--n;

准确地说,这意味着如果你将r,n和d扩展到它们的主要因子分解中,那么可以很容易地取消d,并留下n的修改值,称之为t,然后计算r简直就是:

// compute t from r, n and d
r = r * t;
--n;

快速简便的方法是找到r和d的最大公约数,称之为g:

unsigned long long g = gcd(r, d);
// now one can divide both r and d by g without truncation
r /= g;
unsigned long long d_temp = d / g;
--n;

现在我们可以用d_temp和n做同样的事情(找到最大的公约数)。然而,既然我们知道先验r * n / d是精确的,那么我们也知道gcd(d_temp,n)== d_temp,因此我们不需要计算它。所以我们可以用d_temp来划分n:

unsigned long long g = gcd(r, d);
// now one can divide both r and d by g without truncation
r /= g;
unsigned long long d_temp = d / g;
// now one can divide n by d/g without truncation
unsigned long long t = n / d_temp;
r = r * t;
--n;

清理:

unsigned long long
gcd(unsigned long long x, unsigned long long y)
{
    while (y != 0)
    {
        unsigned long long t = x % y;
        x = y;
        y = t;
    }
    return x;
}

unsigned long long
choose(unsigned long long n, unsigned long long k)
{
    if (k > n)
        throw std::invalid_argument("invalid argument in choose");
    unsigned long long r = 1;
    for (unsigned long long d = 1; d <= k; ++d, --n)
    {
        unsigned long long g = gcd(r, d);
        r /= g;
        unsigned long long t = n / (d / g);
        if (r > std::numeric_limits<unsigned long long>::max() / t)
           throw std::overflow_error("overflow in choose");
        r *= t;
    }
    return r;
}

现在你可以计算选择(67,33)而不会溢出。如果你尝试选择(68,33),你会得到一个例外,而不是一个错误的答案。

答案 2 :(得分:6)

以下例程将使用递归定义和memoization计算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;
}

答案 3 :(得分:4)

请记住

n! / ( n - r )! = n * ( n - 1) * .. * (n - r + 1 )

所以它比n小。所以解决方案是评估n *(n - 1)* ... *(n - r + 1)而不是先计算n!然后分开它。

当然这一切都取决于n和r的相对大小 - 如果r与n相比相对较大,那么它仍然不适合。

答案 4 :(得分:2)

好吧,我必须回答我自己的问题。我正在阅读帕斯卡的三角形,偶然发现我们可以用它来计算组合的数量:

#include <iostream>
#include <boost/cstdint.hpp>

boost::uint64_t Combinations(unsigned int n, unsigned int r)
{
    if (r > n)
        return 0;

    /** We can use Pascal's triange to determine the amount
      * of combinations. To calculate a single line:
      *
      * v(r) = (n - r) / r
      *
      * Since the triangle is symmetrical, we only need to calculate
      * until r -column.
      */

    boost::uint64_t v = n--;

    for (unsigned int i = 2; i < r + 1; ++i, --n)
        v = v * n / i;

    return v;
}

int main()
{
    std::cout << Combinations(52, 5) << std::endl;
}

答案 5 :(得分:1)

获得二项式系数的素数因子化可能是计算它的最有效方法,特别是如果乘法很昂贵。计算阶乘的相关问题肯定是正确的(例如,见Click here)。

这是一个基于Eratosthenes筛选的简单算法,用于计算素数分解。这个想法基本上是通过使用筛子找到它们的质数,但是也可以计算它们的多少倍数落在[1,k]和[n-k + 1,n]的范围内。 Sieve本质上是一个O(n \ log \ log n)算法,但没有完成乘法。找到素数因子分解后所需的实际乘法次数最差为O \ left(\ frac {n \ log \ log n} {\ log n} \ right),并且可能有更快的方法。

prime_factors = []

n = 20
k = 10

composite = [True] * 2 + [False] * n

for p in xrange(n + 1):
if composite[p]:
    continue

q = p
m = 1
total_prime_power = 0
prime_power = [0] * (n + 1)

while True:

    prime_power[q] = prime_power[m] + 1
    r = q

    if q <= k:
        total_prime_power -= prime_power[q]

    if q > n - k:
        total_prime_power += prime_power[q]

    m += 1
    q += p

    if q > n:
        break

    composite[q] = True

prime_factors.append([p, total_prime_power])

 print prime_factors

答案 6 :(得分:0)

首先简化公式。你不想做长期分裂。

答案 7 :(得分:0)

最简单的方式之一:

int nChoosek(int n, int k){
    if (k > n) return 0;
    if (k == 0) return 1;
    return nChoosek(n - 1, k) + nChoosek(n - 1, k - 1);
}

答案 8 :(得分:0)

使用带有long double的脏技巧,可以获得与Howard Hinnant相同的精确度(可能更多):

unsigned long long n_choose_k(int n, int k)
{
    long double f = n;
    for (int i = 1; i<k+1; i++)
        f /= i;
    for (int i=1; i<k; i++)
        f *= n - i;

    unsigned long long f_2 = std::round(f);

    return f_2;
}

这个想法首先要用k来划分!然后乘以n(n-1)...(n-k + 1)。通过反转for循环的顺序可以避免通过double的近似。

答案 9 :(得分:-1)

如果你想100%确定没有溢出,只要最终结果在数字限制内,你可以逐行总结Pascal的三角形:

for (int i=0; i<n; i++) {
    for (int j=0; j<=i; j++) {
        if (j == 0) current_row[j] = 1;
        else current_row[j] = prev_row[j] + prev_row[j-1];
    }
    prev_row = current_row; // assume they are vectors
}
// result is now in current_row[r-1]

然而,这个算法比乘法算法慢得多。所以也许你可以使用乘法生成你知道的所有'安全'的情况,然后从那里使用加法。 (..或者您可以使用BigInt库。)