web-dev-qa-db-ja.com

高速nは、大きなnに対してk mod pを選択しますか?

「大きな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=4269 しかし、私はそれを完全に理解していません

ロジック(combinations(n-1、k-1、p)%p + Combinations(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;
   }  
}
45
John Smith

だから、これがあなたの問題を解決する方法です。

もちろん、あなたは式を知っています:

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と素数は明確に定義されています。-1 見つけることができます。そして、これはフェルマーの定理を使用して行うことができますp-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)を計算して、1回だけ除算できます。

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の場合! pを法として0に等しくなり、(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;
}

編集:上記のソリューションに追加できる最適化がもう1つあります。k!の各倍数の逆数を計算する代わりに、k!(mod p)を計算してから、その数の逆数を計算できます。したがって、べき乗の対数を1回だけ支払う必要があります。もちろん、ここでも各倍数の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;

大規模なkの場合、次の2つの基本的な事実を利用することで、作業を大幅に削減できます。

  1. pが素数の場合、_n!_の素因数分解におけるpの指数は_(n - s_p(n)) / (p-1)_によって与えられます。ここで、s_p(n)は合計ですベースのn表現におけるpの桁数(つまり、_p = 2_の場合は、ポップカウント)。したがって、choose(n,k)の素因数分解におけるpの指数は_(s_p(k) + s_p(n-k) - s_p(n)) / (p-1)_であり、特に、加算k + (n-k)が存在しない場合にのみゼロです。ベースpで実行した場合のキャリー(指数はキャリーの数です)。

  2. ウィルソンの定理:pは、_(p-1)! ≡ (-1) (mod p)_の場合にのみ素数です。

_n!_の因数分解におけるpの指数は通常、

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

pによるchoose(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に対応する数値は、_(j*p + k), 1 ≤ k < p_の_0 ≤ j < q_の積と_(q*p + k), 1 ≤ k ≤ r_の積に寄与します。

pと同じ素数の場合、pを法とする寄与のみに関心があります。完全な実行_j*p + k, 1 ≤ k < p_はそれぞれ__(p-1)!_ modulo pに合同であるため、全体で_(-1)^q_ modulo pの寄与を生成します。最後の(おそらく)不完全な実行は_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 + c_と_A = B + C_、または_a = b + c + 1_と_A = B + C - p_の2つの可能性があります。

計算では、2番目の可能性を事前に排除しましたが、これは必須ではありません。

最初のケースでは、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)
_

2番目のケースでは、

_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が素数であり、apで割り切れない場合、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)演算の複雑さ。kpよりも大幅に大きい場合、O(k)ソリューションよりもはるかに優れています。_k <= p_の場合、オーバーヘッドのあるO(k)ソリューションになります。

13
Daniel Fischer