“大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;
}
}
答案 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
,我们可以通过利用两个基本事实来显着减少工作:
如果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
(指数是进位数)。
威尔逊定理: p
是一个素数,当且仅当(p-1)! ≡ (-1) (mod p)
。
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 + c
和A = B + C
,或a = b + c + 1
和A = 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 m
的 i
直至 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
以避免溢出。