快速n选择k mod p为大n?

时间:2012-04-12 05:57:46

标签: c++ algorithm modular binomial-coefficients

“大n”的意思是数百万。 p是素数。

我试过了 http://apps.topcoder.com/wiki/display/tc/SRM+467 但是这个功能似乎是不正确的(我用144选择6 mod 5进行测试,当它应该给我2时它给我0)

我试过了 http://online-judge.uva.es/board/viewtopic.php?f=22&t=42690 但我完全不理解

我还做了一个memoized递归函数,它使用逻辑(组合(n-1,k-1,p)%p +组合(n-1,k,p)%p)但它给了我堆栈溢出问题,因为n很大

我尝试过卢卡斯定理,但它似乎要么缓慢还是不准确。

我要做的就是为大n创建一个快速/准确的n选择k mod p。如果有人能帮我展示一个很好的实现,我将非常感激。感谢。

根据要求,命中堆栈的memoized版本溢出大n:

std::map<std::pair<long long, long long>, long long> memo;

long long combinations(long long n, long long k, long long p){
   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;

   map<std::pair<long long, long long>, long long>::iterator it;

   if((it = memo.find(std::make_pair(n, k))) != memo.end()) {
        return it->second;
   }
   else
   {
        long long value = (combinations(n-1, k-1,p)%p + combinations(n-1, k,p)%p)%p;
        memo.insert(std::make_pair(std::make_pair(n, k), value));
        return value;
   }  
}

3 个答案:

答案 0 :(得分:51)

所以,以下是解决问题的方法。

当然你知道公式:

comb(n,k) = n!/(k!*(n-k)!) = (n*(n-1)*...(n-k+1))/k! 

(见http://en.wikipedia.org/wiki/Binomial_coefficient#Computing_the_value_of_binomial_coefficients

你知道如何计算分子:

long long res = 1;
for (long long i = n; i > n- k; --i) {
  res = (res * i) % p;
}

现在,当p为素数时, coprime with p 的每个整数的倒数被很好地定义,即可以找到 -1 。并且这可以使用费马定理a p-1 = 1(mod p)=&gt;来完成。 a * a p-2 = 1(mod p),因此 -1 = a p-2 。 现在您需要做的就是实现快速取幂(例如使用二进制方法):

long long degree(long long a, long long k, long long p) {
  long long res = 1;
  long long cur = a;

  while (k) {
    if (k % 2) {
      res = (res * cur) % p;
    }
    k /= 2;
    cur = (cur * cur) % p;
  }
  return res;
}

现在你可以在我们的结果中添加分母:

long long res = 1;
for (long long i = 1; i <= k; ++i) {
  res = (res * degree(i, p- 2)) % p;
}

请注意我在任何地方都使用很长时间来避免类型溢出。当然你不需要做k指数 - 你可以计算k!(mod p)然后除以一次:

long long denom = 1;
for (long long i = 1; i <= k; ++i) {
  denom = (denom * i) % p;
}
res = (res * degree(denom, p- 2)) % p;

编辑:根据@ dbaupp的评论,如果k> = p,那么k!将等于0模p和(k!)^ - 1将不被定义。为了避免这种情况,首先计算p在n *(n-1)...(n-k + 1)和k中的程度!并比较它们:

int get_degree(long long n, long long p) { // returns the degree with which p is in n!
  int degree_num = 0;
  long long u = p;
  long long temp = n;

  while (u <= temp) {
    degree_num += temp / u;
    u *= p;
  }
  return degree_num;
}

long long combinations(int n, int k, long long p) {
  int num_degree = get_degree(n, p) - get_degree(n - k, p);
  int den_degree = get_degree(k, p);

  if (num_degree > den_degree) {
    return 0;
  }
  long long res = 1;
  for (long long i = n; i > n - k; --i) {
    long long ti = i;
    while(ti % p == 0) {
      ti /= p;
    }
    res = (res * ti) % p;
  }
  for (long long i = 1; i <= k; ++i) {
    long long ti = i;
    while(ti % p == 0) {
      ti /= p;
    }
    res = (res * degree(ti, p-2, p)) % p;
  }
  return res;
}

编辑:还有一个优化可以添加到上面的解决方案 - 而不是计算k!中每个倍数的倒数,我们可以计算k!(mod p),然后计算该数字的倒数。因此,我们必须只为指数支付一次对数。当然,我们还要丢弃每个倍数的p除数。我们只需要改变最后一个循环:

long long denom = 1;
for (long long i = 1; i <= k; ++i) {
  long long ti = i;
  while(ti % p == 0) {
    ti /= p;
  }
  denom = (denom * ti) % p;
}
res = (res * degree(denom, p-2, p)) % p;

答案 1 :(得分:13)

对于大型k,我们可以通过利用两个基本事实来显着减少工作:

  1. 如果p是素数,则p的素数因子化中n!的指数由(n - s_p(n)) / (p-1)给出,其中s_p(n)为基数n表示中p的数字之和(因此对于p = 2,它是popcount)。因此p的素数因子分解中choose(n,k)的指数为(s_p(k) + s_p(n-k) - s_p(n)) / (p-1),特别是,当且仅当加法k + (n-k)在基数中执行时没有进位时,它为零p(指数是进位数)。

  2. 威尔逊定理: p是一个素数,当且仅当(p-1)! ≡ (-1) (mod p)

  3. p因子分解中n!的指数通常由

    计算
    long long factorial_exponent(long long n, long long p)
    {
        long long ex = 0;
        do
        {
            n /= p;
            ex += n;
        }while(n > 0);
        return ex;
    }
    

    choose(n,k) p的可分性检查并不是绝对必要的,但首先考虑这一点是合理的,因为通常会出现这种情况,然后工作就会减少:

    long long choose_mod(long long n, long long k, long long p)
    {
        // We deal with the trivial cases first
        if (k < 0 || n < k) return 0;
        if (k == 0 || k == n) return 1;
        // Now check whether choose(n,k) is divisible by p
        if (factorial_exponent(n) > factorial_exponent(k) + factorial_exponent(n-k)) return 0;
        // If it's not divisible, do the generic work
        return choose_mod_one(n,k,p);
    }
    

    现在让我们仔细看看n!。我们将数字≤ n分为p的倍数和互译为p的数字。与

    n = q*p + r, 0 ≤ r < p
    

    p的倍数贡献p^q * q!p (j*p + k), 1 ≤ k < p的{​​{1}}与0 ≤ j < q的乘积以及(q*p + k), 1 ≤ k ≤ r的乘积。

    对于p的互质数字,我们只对模p的贡献感兴趣。每个完整运行j*p + k, 1 ≤ k < p都与(p-1)!p一致,因此它们共生成(-1)^q modulo p的贡献。最后(可能)不完整的运行产生r!p

    所以,如果我们写

    n   = a*p + A
    k   = b*p + B
    n-k = c*p + C
    

    我们得到了

    choose(n,k) = p^a * a!/ (p^b * b! * p^c * c!) * cop(a,A) / (cop(b,B) * cop(c,C))
    

    其中cop(m,r)p ≤ m*p + r所有互译的数字的乘积。

    有两种可能性:a = b + cA = B + C,或a = b + c + 1A = B + C - p

    在我们的计算中,我们事先已经消除了第二种可能性,但这并不重要。

    在第一种情况下,p的明确权力取消,我们留下

    choose(n,k) = a! / (b! * c!) * cop(a,A) / (cop(b,B) * cop(c,C))
                = choose(a,b) * cop(a,A) / (cop(b,B) * cop(c,C))
    

    p分割choose(n,k)的任何权力都来自choose(a,b) - 在我们的情况下,没有,因为我们之前已经消除了这些案例 - 尽管{{1}不必是一个整数(例如cop(a,A) / (cop(b,B) * cop(c,C))),当考虑模choose(19,9) (mod 5)时,p简化为cop(m,r),因此,(-1)^m * r!a = b + c取消,我们留下了

    (-1)

    在第二种情况下,我们找到

    choose(n,k) ≡ choose(a,b) * choose(A,B) (mod p)
    

    choose(n,k) = choose(a,b) * p * cop(a,A)/ (cop(b,B) * cop(c,C)) 以来。最后一位数的进位表示a = b + c + 1,因此模A < B

    p

    (我们可以用模乘逆乘法来代替除法,或者将它视为有理数的同余,意味着分子可以被p * cop(a,A) / (cop(b,B) * cop(c,C)) ≡ 0 = choose(A,B) 整除)。无论如何,我们再次找到

    p

    现在我们可以重复choose(n,k) ≡ choose(a,b) * choose(A,B) (mod p) 部分。

    示例:

    choose(a,b)

    现在实施:

    choose(144,6) (mod 5)
    144 = 28 * 5 + 4
      6 =  1 * 5 + 1
    choose(144,6) ≡ choose(28,1) * choose(4,1) (mod 5)
                  ≡ choose(3,1) * choose(4,1) (mod 5)
                  ≡ 3 * 4 = 12 ≡ 2 (mod 5)
    
    choose(12349,789) ≡ choose(2469,157) * choose(4,4)
                      ≡ choose(493,31) * choose(4,2) * choose(4,4
                      ≡ choose(98,6) * choose(3,1) * choose(4,2) * choose(4,4)
                      ≡ choose(19,1) * choose(3,1) * choose(3,1) * choose(4,2) * choose(4,4)
                      ≡ 4 * 3 * 3 * 1 * 1 = 36 ≡ 1 (mod 5)
    

    要计算模逆,可以使用费马(所谓的小)定理

      

    如果// Preconditions: 0 <= k <= n; p > 1 prime long long choose_mod_one(long long n, long long k, long long p) { // For small k, no recursion is necessary if (k < p) return choose_mod_two(n,k,p); long long q_n, r_n, q_k, r_k, choose; q_n = n / p; r_n = n % p; q_k = k / p; r_k = k % p; choose = choose_mod_two(r_n, r_k, p); // If the exponent of p in choose(n,k) isn't determined to be 0 // before the calculation gets serious, short-cut here: /* if (choose == 0) return 0; */ choose *= choose_mod_one(q_n, q_k, p); return choose % p; } // Preconditions: 0 <= k <= min(n,p-1); p > 1 prime long long choose_mod_two(long long n, long long k, long long p) { // reduce n modulo p n %= p; // Trivial checks if (n < k) return 0; if (k == 0 || k == n) return 1; // Now 0 < k < n, save a bit of work if k > n/2 if (k > n/2) k = n-k; // calculate numerator and denominator modulo p long long num = n, den = 1; for(n = n-1; k > 1; --n, --k) { num = (num * n) % p; den = (den * k) % p; } // Invert denominator modulo p den = invert_mod(den,p); return (num * den) % p; } 为素数且p不能被a整除,则p

    并将逆计算为a^(p-1) ≡ 1 (mod p),或使用适用于更广泛参数的方法,扩展欧几里德算法或连续分数展开,它们为任何一对互质(正)整数提供模块化逆:

    a^(p-2) (mod p)

    与计算long long invert_mod(long long k, long long m) { if (m == 0) return (k == 1 || k == -1) ? k : 0; if (m < 0) m = -m; k %= m; if (k < 0) k += m; int neg = 1; long long p1 = 1, p2 = 0, k1 = k, m1 = m, q, r, temp; while(k1 > 0) { q = m1 / k1; r = m1 % k1; temp = q*p1 + p2; p2 = p1; p1 = temp; m1 = k1; k1 = r; neg = !neg; } return neg ? m - p2 : p2; } 一样,这是一种a^(p-2) (mod p)算法,对于某些输入,它明显更快(实际上是O(log p),因此对于小O(min(log k, log p))和大k },它的速度要快得多,对其他人来说速度要慢一些。

    总的来说,这种方式我们需要计算最多O(log_p k)二项式系数模p,其中每个二项式系数最多需要O(p)运算,产生O的总复杂度(p * log_p k)运营。 当p明显大于k时,这比p解决方案要好得多。对于O(k),它会减少到k <= p解决方案并带来一些开销。

答案 2 :(得分:0)

如果您不止一次计算它,还有另一种更快的方法。我打算用 python 发布代码,因为它可能是最容易转换成另一种语言的,尽管我会把 C++ 代码放在最后。

计算一次

蛮力:

def choose(n, k, m):
    ans = 1
    for i in range(k): ans *= (n-i)
    for i in range(k): ans //= i
    return ans % m

但是计算可以得到非常大的数字,所以我们可以使用模块化的空气数学技巧来代替:

(a * b) mod m = (a mod m) * (b mod m) mod m

(a / (b*c)) mod m = (a mod m) / ((b mod m) * (c mod m) mod m)

(a / b) mod m = (a mod m) * (b mod m)^-1

注意最后一个等式末尾的 ^-1。这是 b mod m 的乘法逆。它基本上意味着 ((b mod m) * (b mod m)^-1) mod m = 1,就像 a * a^-1 = a * 1/a = 1 与(非零)整数一样。

这可以通过几种方式计算,其中之一是扩展欧几里德算法:

def multinv(n, m):
    ''' Multiplicative inverse of n mod m '''
    if m == 1: return 0
    m0, y, x = m, 0, 1

    while n > 1:
        y, x = x - n//m*y, y
        m, n = n%m, m
    
    return x+m0 if x < 0 else x

请注意,另一种方法,求幂,仅当 m 为素数时才有效。如果是,您可以这样做:

def powmod(b, e, m):
    ''' b^e mod m '''
    # Note: If you use python, there's a built-in pow(b, e, m) that's probably faster
    # But that's not in C++, so you can convert this instead:
    P = 1
    while e:
        if  e&1: P = P * b % m
        e >>= 1, b = b * b % m
    return P

def multinv(n, m):
    ''' Multiplicative inverse of n mod m, only if m is prime '''
    return powmod(n, m-2, m)
    

但请注意,扩展欧几里得算法往往仍然运行得更快,即使它们在技术上具有相同的时间复杂度 O(log m),因为它具有较低的常数因子。

现在是完整的代码:

def multinv(n, m):
    ''' Multiplicative inverse of n mod m in log(m) '''
    if m == 1: return 0
    m0, y, x = m, 0, 1

    while n > 1:
        y, x = x - n//m*y, y
        m, n = n%m, m
    
    return x+m0 if x < 0 else x


def choose(n, k, m):
    num, den = 1, 1
    for i in range(k): num = num * (n-i) % m
    for i in range(k): den = den * i % m
    return num * multinv(den, m)

多次查询

我们可以分别计算分子和分母,然后将它们合并。但请注意,我们为分子计算的乘积是 n * (n-1) * (n-2) * (n-3) ... * (n-k+1)。如果您曾经了解过一种叫做前缀和的东西,那就非常相似了。所以让我们应用它。

预先计算 fact[i] = i! mod mi 直至 n 的最大值,可能是 1e7(千万)。那么,分子是(fact[n] * fact[n-k]^-1) mod m,分母是fact[k]。所以我们可以计算choose(n, k, m) = fact[n] * multinv(fact[n-k], m) % m * multinv(fact[k], m) % m

Python 代码:

MAXN = 1000 # Increase if necessary
MOD = 10**9+7 # A common mod that's used, change if necessary

fact = [1]
for i in range(1, MAXN+1):
    fact.append(fact[-1] * i % MOD)

def multinv(n, m):
    ''' Multiplicative inverse of n mod m in log(m) '''
    if m == 1: return 0
    m0, y, x = m, 0, 1

    while n > 1:
        y, x = x - n//m*y, y
        m, n = n%m, m
    
    return x+m0 if x < 0 else x


def choose(n, k, m):
    return fact[n] * multinv(fact[n-k]) % m
                   * multinv(fact[k]) % m

C++ 代码:

#include <iostream>
using namespace std;

const int MAXN = 1000; // Increase if necessary
const int MOD = 1e9+7; // A common mod that's used, change if necessary

int fact[MAXN+1];

int multinv(int n, int m) {
    /* Multiplicative inverse of n mod m in log(m) */
    if (m == 1) return 0;
    int m0 = m, y = 0, x = 1, t;

    while (n > 1) {
        t = y;
        y = x - n/m*y;
        x = t;
        
        t = m;
        m = n%m;
        n = t;
    }
    
    return x<0 ? x+m0 : x;
}

int choose(int n, int k, int m) {
    return (long long) fact[n]
         * multinv(fact[n-k], m) % m
         * multinv(fact[k], m) % m;
}

int main() {
    fact[0] = 1;
    for (int i = 1; i <= MAXN; i++) {
        fact[i] = (long long) fact[i-1] * i % MOD;
    }

    cout << choose(4, 2, MOD) << '\n';
    cout << choose(1e6, 1e3, MOD) << '\n';
}

请注意,我将强制转换为 long long 以避免溢出。