【问题标题】:Fast n choose k mod p for large n?快速 n 为大 n 选择 k mod p?
【发布时间】:2012-04-24 11:28:27
【问题描述】:

我所说的“大 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 但我不完全理解它

我还制作了一个使用逻辑 (combinations(n-1, k-1, p)%p + combination(n-1, k, p)%p) 的记忆递归函数,但它给了我堆栈溢出问题,因为 n 很大

我已经尝试过卢卡斯定理,但它似乎很慢或不准确。

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

根据要求,命中堆栈溢出的记忆版本对于大 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;
   }  
}

【问题讨论】:

  • 你需要知道确切的提醒还是知道数字是否均匀可以被p整除? (n 选择 k mod p == 0)
  • 不确定我是否理解这个问题。 n 选择 k mod p 的答案需要准确/准确。
  • 组合函数返回什么(为什么需要3个参数)
  • combinations 函数需要三个参数,因为它正在寻找 (n 选择 k) mod p
  • 所以需要计算组合(n, k)%p?

标签: c++ algorithm modular binomial-coefficients


【解决方案1】:

那么,这里是您可以解决问题的方法。

你当然知道公式:

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 是素数,因此 与 p 互质的每个整数的倒数都已明确定义,即可以找到 a-1。这可以使用费马定理 ap-1=1(mod p) => a*ap-2=1(mod p) 等 a-1=ap-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;
}

请注意,我在任何地方都使用 long long 以避免类型溢出。当然你不需要做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 的评论 if k >= p the 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!(mod p) 然后计算该数字的倒数,而不是计算 k! 中每个倍数的倒数。因此,我们只需要支付一次取幂的对数。当然,我们必须再次丢弃每个倍数的 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;

【讨论】:

  • 你只是在计算n*(n-1)*...*(n-k+1) * (k!)^-1吗?这仅在 k &lt; p 时定义,否则在 k! == 0 且不存在逆时。
  • 如果 k > p 则应特别注意计算 p 在 n*(n-1)*...*(n-k+1) 和 k 中的次数!然后取消这些事件
  • 我认为“计算 p 的度数并取消”位并非微不足道。至少,没有效率。
  • 这似乎类似于我在我发布的第一个链接中展示的实现(144 如何选择 6 mod 5 不起作用等)
  • 我已经更新了我的帖子,请再次阅读。抱歉弄错了。
【解决方案2】:

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

  1. 如果p 是素数,则pn! 的素因式分解中的指数由(n - s_p(n)) / (p-1) 给出,其中s_p(n)n 在基本p 表示(所以对于p = 2,它是popcount)。因此pchoose(n,k) 的素因数分解中的指数是(s_p(k) + s_p(n-k) - s_p(n)) / (p-1),特别是当且仅当在基数p 中执行加法时k + (n-k) 没有进位(指数是携带次数)。

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

pn! 的因式分解中的指数通常由下式计算

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

pchoose(n,k) 的整除性检查并不是绝对必要的,但首先进行检查是合理的,因为通常是这样,然后工作量就会减少:

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 互质的数字为0 ≤ j &lt; q 贡献了(j*p + k), 1 ≤ k &lt; p 的乘积,以及(q*p + k), 1 ≤ k ≤ r 的乘积。

对于与p 互质的数字,我们只对以p 为模的贡献感兴趣。每个完整的运行j*p + k, 1 ≤ k &lt; p 都与(p-1)!p 一致,因此它们总共产生(-1)^qp 的贡献。最后一次(可能)不完整的运行产生r! modulo 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) - 在我们的例子中,不会有,因为我们之前已经消除了这些情况 - 而且,虽然 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 &lt; 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 和大的 p,它要快得多),对于其他输入它是慢一点。

总的来说,这种方式我们最多需要计算 O(log_p k) 个二项式系数以 p 为模,其中每个二项式系数最多需要 O(p) 次运算,总复杂度为 O(p*log_p k)操作。 当k 明显大于p 时,这比O(k) 解决方案要好得多。对于k &lt;= p,它减少为O(k) 解决方案,但有一些开销。

【讨论】:

  • 你能把你的算法总结一下吗?我很难按照这些步骤进行操作。
  • 你能告诉我你有什么困难吗?如果我不必完全猜测哪些部分可能会对无法读懂我的想法的人造成问题,那会更容易做到。
  • 您似乎在第一部分通过卢卡斯定理的结果运行一个循环(以递归函数为幌子),并在第二部分使用乘法逆计算 nCk mod p? (这是我正在寻找的东西)。卢卡斯定理会处理 p 很小的情况。
  • 是的,就是这样(我写的时候不知道有人麻烦地提出了关系的定理,因此没有提到卢卡斯大师;现在我知道了,我应该补充对它的引用)。
【解决方案3】:

如果您计算不止一次,还有另一种更快的方法。我将在 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)。如果您曾经了解过一种叫做 prefix sums 的东西,那么这非常相似。所以让我们应用它。

i 预先计算fact[i] = i! mod m,直到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 以避免溢出。

【讨论】:

  • 谢谢!我发现这很有帮助。但是,在最后一个 Python 版本中,它缺少对 multinv() 的调用中的最后一个 m 参数。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-06-20
相关资源
最近更新 更多