是否存在“n选r模m”的数学技术?

时间:2012-11-08 06:46:18

标签: c

当n,r,m是非常大的数时,如何计算编程中的nCr%m(即"(n选择r)模数m")

4 个答案:

答案 0 :(得分:1)

我假设你想为大数量计算nCr类型的东西,并搜索一些可能优化的库函数。在这种情况下,您可以查看GNU Scientific Library

答案 1 :(得分:1)

有些技术可以降低计算二项式系数模数整数m的复杂度,前提是m的素因子都不是太大。将m分得太高了。

第一步是分解m

m = ∏ (p_i ^ e_i)

然后计算以这些主要权力为模的二项式系数,并将结果与​​Chinese remainder theorem结合起来。

模拟素数(e_i == 1)的二项式系数的计算可以比一般情况更容易计算,参见例如this answer,但它也可以包含在那里。

对于素数pn >= 0,让我们定义

F(p, n) =   ∏ k   = p^(n/p) * (n/p)!
         1<=k<=n
           p | k

G(p, n) =    ∏ k
          1<=k<=n
         gcd(k,n)=1

然后我们

n! = F(p, n) * G(p, n)

并迭代地对(n/p)!中出现的F(p, n)使用相同的拆分,

           m
n! = p^K * ∏ G(p, n/(p^j))
          j=0

其中p^m <= n < p^(m+1)G(p, x)中的所有因子都与p^e相互作用,因此二项式系数的分母中的相应因子可以反模p^e,如果我们找到一种有效的方法来计算G(p, x) 1}} modulo p^e,我们有一种有效的方法来计算二项式系数p^e

对于二项式系数,我们有

n! / (r! * (n-r)!) = p^M * (∏ G(p, (n/p^j)) * [ ∏ G(p, r/(p^j)) * ∏ G(p, (n-r)/(p^j)) ]^(-1)

H(p, e, n) = G(p, n) % (p^e)。关键的一点是,所有数字互为p^e且不超过p^e的数字的乘积非常简单。它与-1p^e一致,除非p = 2e > 2,在这种情况下它与1一致。

所以

H(p, e, n) ≡ (-1)^(n/(p^e)) * H(p, e, n % (p^e)) (mod p^e)

(除非p = 2e > 2,在这种情况下第一个因素是1),我们只需为H(p, e, k)计算0 <= k < p^e,然后我们就可以查找结果。

代码:

// invert k modulo p, k and p are supposed coprime
unsigned long long invertMod(unsigned long long k, unsigned long long p) {
    unsigned long long q, pn = 1, po = 0, r = p, s = k;
    unsigned odd = 1;
    do {
        q = r/s;
        q = pn*q + po;
        po = pn;
        pn = q;
        q = r%s;
        r = s;
        s = q;
        odd ^= 1;
    }while(pn < p);
    return odd ? p-po : po;
}

// Calculate the binomial coefficient (n choose k) modulo (prime^exponent)
// requires prime to be a prime, exponent > 0, and 0 <= k <= n,
// furthermore supposes prime^exponent < 2^32, otherwise intermediate
// computations could have mathematical results out of range.
// If k or (n-k) is small, a direct computation would be more efficient.
unsigned long long binmod(unsigned long long prime, unsigned exponent,
                          unsigned long long n, unsigned long long k) {
    // The modulus, prime^exponent
    unsigned long long ppow = 1;
    // We suppose exponent is small, so that exponentiation by repeated
    // squaring wouldn't gain much.
    for(unsigned i = 0; i < exponent; ++i) {
        ppow *= prime;
    }
    // array of remainders of products
    unsigned long long *remainders = malloc(ppow * sizeof *remainders);
    if (!remainders) {
        fprintf(stderr, "Allocation failure\n");
        exit(EXIT_FAILURE);
    }
    for(unsigned long long i = 1; i < ppow; ++i) {
        remainders[i] = i;
    }
    for(unsigned long long i = 0; i < ppow; i += prime) {
        remainders[i] = 1;
    }
    for(unsigned long long i = 2; i < ppow; ++i) {
        remainders[i] *= remainders[i-1];
        remainders[i] %= ppow;
    }

    // Now to business.
    unsigned long long pmult = 0, ntemp = n, ktemp = k, mtemp = n-k,
                       numer = 1, denom = 1, q, r, f;
    if (prime == 2 && exponent > 2) {
        f = 0;
    } else {
        f = 1;
    }
    while(ntemp) {
        r = ntemp % ppow;
        q = ntemp / ppow;
        numer *= remainders[r];
        numer %= ppow;
        if (q & f) {
            numer = ppow - numer;
        }
        ntemp /= prime;
        pmult += ntemp;
    }
    while(ktemp) {
        r = ktemp % ppow;
        q = ktemp / ppow;
        denom *= remainders[r];
        denom %= ppow;
        if (q & f) {
            denom = ppow - denom;
        }
        ktemp /= prime;
        pmult -= ktemp;
    }
    while(mtemp) {
        r = mtemp % ppow;
        q = mtemp / ppow;
        denom *= remainders[r];
        denom %= ppow;
        if (q & f) {
            denom = ppow - denom;
        }
        mtemp /= prime;
        pmult -= mtemp;
    }
    // free memory before returning, we don't use it anymore
    free(remainders);
    if (pmult >= exponent) {
        return 0;
    }
    while(pmult > 0) {
        numer = (numer * prime) % ppow;
        --pmult;
    }
    return (numer * invertMod(denom, ppow)) % ppow;
}

n choose k modulo p^e步骤中计算O(p^e + log n)

答案 2 :(得分:1)

首先,它取决于你所说的“非常大”。如果您的问题仅仅是中间值对于标准64位整数而言太大,那么您可以使用gmpBigInteger之类的内容。

所涉及的数字可能会变得如此之大,以至于你的内存或耐心都会耗尽,而你将无法计算中间值来完成精确度。在这种情况下,最好的办法是首先确定每个因子的素数因子分解,使用这些因子分解来确定二项式的因子分解,然后使用每个中间步骤的模数将该因式分解出来。

您需要一个包含n的所有素数的列表。

以下是伪代码。我正在使用int,但您应该将其替换为您正在使用的任何大型数据库。

int factorial_prime_power(int f, int p) {
    // When p is prime, returns k such that p^k divides f!
    int k = 0;
    while (f > p) {
        f = f / p;
        k = k + f;
    }
    return k;
}
int binomial_prime_power(int n, int r, int p) {
    // when p is prime, returns k such that p^k divides nCr
    return factorial_prime_power(n,p) - factorial_prime_power(r,p) - factorial_prime_power(n-r,p);
}
int powmod(int p, int k, int m) {
    // quickly calculates p^k mod m
    int res = 1;
    int q = p;
    int j = k;
    while (j > 0) {
        // invariant:  p^k is congruent to res * q^j
        if (j is odd) {
            res = (res * q) % m;
            j = (j-1)/2;
        } else {
            j = j / 2;
        }
        q = (q * q) % m;
    }
    return res;
}
int big_binomial(int n, int r, int m) {
    if (n < r or r < 0) {
        return 0;
    }
    int res = 1;
    for(p in all primes from 2 to n) {
        k = binomial_prime_power(n,r,p);
        res = (res * powmod(p,k,m)) % m;
    }
    return res;
}

答案 3 :(得分:0)

以大块的方式做。

例如: n!/(r!(nr)!)= 1 * 2 * ... * n /(1 * 2 * ... r * 1 * 2 * ... *(nr))= 1/1 * 1 * 2/2 * 2 * 3/3 * 3 * ...... * ....

每个块都很容易计算,因此您应该避免计算中的溢出。计算块并将其乘以当前结果。

此外,值得在此之前对n和r进行模数化。