web-dev-qa-db-fra.com

Rapide n choisissez k mod p pour grand n?

Ce que je veux dire par "grand n" est quelque chose dans les millions. p est premier.

J'ai essayé http://apps.topcoder.com/wiki/display/tc/SRM+467 Mais la fonction semble être incorrecte (je l'ai testée avec 144 choisissez 6 mod 5 et ça donne moi 0 quand ça devrait me donner 2)

J'ai essayé http://online-judge.uva.es/board/viewtopic.php?f=22&t=4269 Mais je ne le comprends pas complètement

J'ai également créé une fonction récursive mémorisée qui utilise la logique (combinaisons (n-1, k-1, p)% p + combinaisons (n-1, k, p)% p) ​​mais cela me donne des problèmes de débordement de pile car n est grand

J'ai essayé Lucas Theorem mais il semble être lent ou inexact.

Tout ce que j'essaie de faire, c'est de créer un n rapide/précis n choisir k mod p pour un grand n. Si quelqu'un pouvait m'aider à montrer une bonne implémentation pour cela, je serais très reconnaissant. Merci.

Comme demandé, la version mémorisée qui atteint les débordements de pile pour les grands 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

Voici donc comment résoudre votre problème.

Bien sûr, vous connaissez la formule:

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

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

Vous savez calculer le numérateur:

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

Maintenant, comme p est premier, l'inverse de chaque entier qui est coprime avec p est bien défini, c'est-à-dire a-1 peut être trouvé. Et cela peut être fait en utilisant le théorème de Fermat ap-1= 1 (mod p) => a * ap-2= 1 (mod p) et donc a-1= ap-2. Il ne vous reste plus qu'à implémenter l'exponentiation rapide (par exemple en utilisant la méthode binaire):

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;
}

Et maintenant, vous pouvez ajouter le dénominateur à notre résultat:

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

Veuillez noter que j'utilise long long partout pour éviter le débordement de type. Bien sûr, vous n'avez pas besoin de faire des exponentiations k - vous pouvez calculer k! (Mod p) puis diviser une seule fois:

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

EDIT: selon le commentaire de @ dbaupp si k> = p le k! sera égal à 0 modulo p et (k!) ^ - 1 ne sera pas défini. Pour éviter cela, calculez d'abord le degré avec lequel p est dans n * (n-1) ... (n-k + 1) et dans k! et les comparer:

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;
}

EDIT: Il y a une autre optimisation qui peut être ajoutée à la solution ci-dessus - au lieu de calculer le nombre inverse de chaque multiple dans k !, nous pouvons calculer k! (Mod p) puis calculer l'inverse de ce nombre. Nous devons donc payer le logarithme pour l'exponentiation une seule fois. Bien sûr, encore une fois, nous devons éliminer les p diviseurs de chaque multiple. Il suffit de changer la dernière boucle avec ceci:

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;

Pour les grands k, nous pouvons réduire considérablement le travail en exploitant deux faits fondamentaux:

  1. Si p est un nombre premier, l'exposant de p dans la factorisation principale de n! Est donné par (n - s_p(n)) / (p-1), Où s_p(n) est la somme des chiffres de n dans la représentation de base p (donc pour p = 2, c'est popcount). Ainsi l'exposant de p dans la factorisation première de choose(n,k) est (s_p(k) + s_p(n-k) - s_p(n)) / (p-1), En particulier, il est nul si et seulement si l'addition k + (n-k) a pas de report quand effectué dans la base p (l'exposant est le nombre de reports).

  2. théorème de Wilson:p est un nombre premier, si et seulement si (p-1)! ≡ (-1) (mod p).

L'exposant de p dans la factorisation de n! Est généralement calculé par

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

La vérification de la divisibilité de choose(n,k) par p n'est pas strictement nécessaire, mais il est raisonnable de l'avoir en premier, car ce sera souvent le cas, puis c'est moins de travail:

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);
}

Voyons maintenant de plus près n!. Nous séparons les nombres ≤ n En multiples de p et les nombres coprime à p. Avec

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

Les multiples de p contribuent p^q * q!. Les nombres coprimes à p contribuent au produit de (j*p + k), 1 ≤ k < p Pour 0 ≤ j < q Et au produit de (q*p + k), 1 ≤ k ≤ r.

Pour les nombres coprimes à p nous ne serons intéressés que par la contribution modulo p. Chacune des exécutions complètes j*p + k, 1 ≤ k < p Est conforme à (p-1)! Modulo p, donc au total, elles produisent une contribution de (-1)^q Modulo p. La dernière exécution (éventuellement) incomplète produit r! Modulo p.

Donc, si nous écrivons

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

on a

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

cop(m,r) est le produit de tous les nombres coprimes à p qui sont ≤ m*p + r.

Il existe deux possibilités, a = b + c Et A = B + C, Ou a = b + c + 1 Et A = B + C - p.

Dans notre calcul, nous avons préalablement éliminé la deuxième possibilité, mais ce n'est pas essentiel.

Dans le premier cas, les pouvoirs explicites de p s'annulent, et nous nous retrouvons avec

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))

Tous les pouvoirs de p divisant choose(n,k) proviennent de choose(a,b) - dans notre cas, il n'y en aura pas, puisque nous avons éliminé ces cas auparavant - et, bien que cop(a,A) / (cop(b,B) * cop(c,C)) n'a pas besoin d'être un entier (considérez par exemple choose(19,9) (mod 5)), lorsque vous considérez l'expression modulo p, cop(m,r) se réduit à (-1)^m * r!, donc, puisque a = b + c, le (-1) annule et nous nous retrouvons avec

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

Dans le second cas, on trouve

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

depuis a = b + c + 1. Le report dans le dernier chiffre signifie que A < B, Donc modulo p

p * cop(a,A) / (cop(b,B) * cop(c,C)) ≡ 0 = choose(A,B)

(où nous pouvons soit remplacer la division par une multiplication par l'inverse modulaire, soit la considérer comme une congruence de nombres rationnels, ce qui signifie que le numérateur est divisible par p). Quoi qu'il en soit, nous trouvons à nouveau

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

Maintenant, nous pouvons récidiver pour la partie choose(a,b).

Exemple:

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)

Maintenant l'implémentation:

// 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;
}

Pour calculer l'inverse modulaire, vous pouvez utiliser le théorème de Fermat (soi-disant petit)

Si p est premier et a non divisible par p, alors a^(p-1) ≡ 1 (mod p).

et calculez l'inverse comme a^(p-2) (mod p), ou utilisez une méthode applicable à une plus large gamme d'arguments, l'algorithme euclidien étendu ou l'expansion de fraction continue, qui vous donne l'inverse modulaire pour toute paire d'entiers coprimes (positifs):

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;
}

Comme pour le calcul de a^(p-2) (mod p), il s'agit d'un algorithme O(log p), pour certaines entrées, il est nettement plus rapide (c'est en fait O(min(log k, log p)), donc pour les petits k et les grands p, c'est beaucoup plus rapide), pour d'autres c'est plus lent.

Dans l'ensemble, de cette façon, nous devons calculer au plus O (log_p k) coefficients binomiaux modulo p, où chaque coefficient binomial a besoin au plus O(p) opérations, ce qui donne un total complexité des opérations O (p * log_p k). Lorsque k est significativement plus grande que p, c'est bien mieux que la solution O(k). Pour k <= p, il se réduit à la solution O(k) avec une surcharge.

13
Daniel Fischer