4 votes

Rendre le crible d'Ératosthène plus efficace en mémoire en python ?

Problème de contrainte de mémoire du crible d'Eratosthène

Je suis actuellement en train de mettre en œuvre une version du crible d'Eratosthène pour un problème de Kattis, cependant, je rencontre des contraintes de mémoire que mon implémentation ne parvient pas à surpasser.

Voici un lien vers le énoncé du problème. En bref, le problème veut que je retourne d'abord le nombre de nombres premiers inférieur ou égal à n et ensuite résoudre pour un certain nombre de requêtes si un nombre i est premier ou non. Il y a une contrainte d'utilisation de mémoire de 50 Mo ainsi que l'utilisation uniquement des bibliothèques standard de python (pas de numpy, etc.). La contrainte de mémoire est là où je suis bloqué.

Voici mon code jusqu'à présent:

import sys

def sieve_of_eratosthenes(xs, n):
    count = len(xs) + 1
    p = 3 # Commencer à trois
    index = 0
    while p*p < n:
        for i in range(index + p, len(xs), p):
            if xs[i]:
                xs[i] = 0
                count -= 1

        temp_index = index
        for i in range(index + 1, len(xs)):
            if xs[i]:
                p = xs[i]
                temp_index += 1
                break
            temp_index += 1
        index = temp_index

    return count

def isPrime(xs, a):
    if a == 1:
        return False
    if a == 2:
        return True
    if not (a & 1):
        return False
    return bool(xs[(a >> 1) - 1])

def main():
    n, q = map(int, sys.stdin.readline().split(' '))
    odds = [num for num in range(2, n+1) if (num & 1)]
    print(sieve_of_eratosthenes(odds, n))

    for _ in range(q):
        query = int(input())
        if isPrime(odds, query):
            print('1')
        else:
            print('0')

if __name__ == "__main__":
    main()

J'ai apporté certaines améliorations jusqu'à présent, comme le fait de ne conserver qu'une liste de tous les nombres impairs, ce qui divise par deux l'utilisation de la mémoire. Je suis également certain que le code fonctionne comme prévu lors du calcul des nombres premiers (sans obtenir de mauvaise réponse). Ma question maintenant est, comment puis-je rendre mon code encore plus efficace en termes de mémoire? Devrais-je utiliser d'autres structures de données? Remplacer ma liste d'entiers par des booléens? Bitarray?

Tout conseil est grandement apprécié!

ÉDITION

Après quelques ajustements au code en python, je me suis heurté à un mur où mon implémentation d'un crible segmenté ne parvenait pas à respecter les exigences en matière de mémoire.

À la place, j'ai choisi de mettre en œuvre la solution en Java, ce qui a nécessité très peu d'efforts. Voici le code:

 public int sieveOfEratosthenes(int n){
    sieve = new BitSet((n+1) / 2);
    int count = (n + 1) / 2;

    for (int i=3; i*i <= n; i += 2){
      if (isComposite(i)) {
        continue;
      }

      // Incrémenter de deux, en sautant tous les nombres pairs
      for (int c = i * i; c <= n; c += 2 * i){
        if(!isComposite(c)){
          setComposite(c);
          count--;
        }
      }
    }

    return count;

  }

  public boolean isComposite(int k) {
    return sieve.get((k - 3) / 2); // Puisque nous ne gardons pas trace des nombres pairs
  }

  public void setComposite(int k) {
    sieve.set((k - 3) / 2); // Puisque nous ne gardons pas trace des nombres pairs
  }

  public boolean isPrime(int a) {
    if (a < 3)
      return a > 1;

    if (a == 2)
      return true;

    if ((a & 1) == 1)
      return !isComposite(a);
    else
      return false;

  }

  public void run() throws Exception{
    BufferedReader scan = new BufferedReader(new InputStreamReader(System.in));
    String[] line = scan.readLine().split(" ");

    int n = Integer.parseInt(line[0]); int q = Integer.parseInt(line[1]);
    System.out.println(sieveOfEratosthenes(n));

    for (int i=0; i < q; i++){
      line = scan.readLine().split(" ");
      System.out.println( isPrime(Integer.parseInt(line[0])) ? '1' : '0');
    }
  }

Personnellement, je n'ai pas trouvé de moyen de mettre en œuvre cette solution BitSet en Python (en n'utilisant que la bibliothèque standard).

Si quelqu'un trouve une mise en œuvre intéressante du problème en python, en utilisant un crible segmenté, bitarray ou autre chose, je serais intéressé de voir la solution.

3voto

Mark Ransom Points 132545

Il y a un truc que j'ai appris juste hier - si vous divisez les nombres en groupes de 6, seuls 2 des 6 peuvent être premiers. Les autres peuvent être divisés de manière égale par 2 ou 3. Cela signifie qu'il faut seulement 2 bits pour suivre la primalité de 6 nombres ; un octet contenant 8 bits peut suivre la primalité pour 24 nombres! Cela réduit considérablement les besoins en mémoire de votre crible.

Dans Python 3.7.5 64 bits sur Windows 10, le code suivant n'a pas dépassé 36,4 Mo.

remainder_bit = [0, 0x01, 0, 0, 0, 0x02,
                 0, 0x04, 0, 0, 0, 0x08,
                 0, 0x10, 0, 0, 0, 0x20,
                 0, 0x40, 0, 0, 0, 0x80]

def is_prime(xs, a):
    if a <= 3:
        return a > 1
    index, rem = divmod(a, 24)
    bit = remainder_bit[rem]
    if not bit:
        return False
    return not (xs[index] & bit)

def sieve_of_eratosthenes(xs, n):
    count = (n // 3) + 1 # soustraire 1 et 4, ajouter 2 3 et 5
    p = 5
    while p*p <= n:
        if is_prime(xs, p):
            for i in range(5 * p, n + 1, p):
                index, rem = divmod(i, 24)
                bit = remainder_bit[rem]
                if bit and not (xs[index] & bit):
                    xs[index] |= bit
                    count -= 1
        p += 2
        if is_prime(xs, p):
            for i in range(5 * p, n + 1, p):
                index, rem = divmod(i, 24)
                bit = remainder_bit[rem]
                if bit and not (xs[index] & bit):
                    xs[index] |= bit
                    count -= 1
        p += 4

    return count

def init_sieve(n):
    return bytearray((n + 23) // 24)

n = 100000000
xs = init_sieve(n)
sieve_of_eratosthenes(xs, n)
5761455
sum(is_prime(xs, i) for i in range(n+1))
5761455

Edit: la clé pour comprendre comment cela fonctionne est qu'un crible crée un motif répétitif. Pour les nombres premiers 2 et 3, le motif se répète tous les 2*3 ou 6 nombres, et de ces 6, 4 ont été rendus impossibles à être premiers, ne laissant que 2. Il n'y a rien pour limiter vos choix de nombres premiers pour générer le motif, sauf peut-être la loi des rendements décroissants. J'ai décidé d'essayer d'ajouter 5 au mélange, faisant ainsi que le motif se répète tous les 2*3*5=30 nombres. Sur ces 30 nombres, seuls 8 peuvent être premiers, ce qui signifie que chaque octet peut suivre 30 nombres au lieu des 24 ci-dessus ! Cela vous donne un avantage de 20% dans l'utilisation de la mémoire.

Voici le code mis à jour. J'ai aussi simplifié un peu et enlevé le comptage des nombres premiers au fur et à mesure.

remainder_bit30 = [0,    0x01, 0,    0,    0,    0,    0, 0x02, 0,    0,
                   0,    0x04, 0,    0x08, 0,    0,    0, 0x10, 0,    0x20,
                   0,    0,    0,    0x40, 0,    0,    0, 0,    0,    0x80]

def is_prime(xs, a):
    if a <= 5:
        return (a > 1) and (a != 4)
    index, rem = divmod(a, 30)
    bit = remainder_bit30[rem]
    return (bit != 0) and not (xs[index] & bit)

def sieve_of_eratosthenes(xs):
    n = 30 * len(xs) - 1
    p = 0
    while p*p < n:
        for offset in (1, 7, 11, 13, 17, 19, 23, 29):
            p += offset
            if is_prime(xs, p):
                for i in range(p * p, n + 1, p):
                    index, rem = divmod(i, 30)
                    if index < len(xs):
                        bit = remainder_bit30[rem]
                        xs[index] |= bit
            p -= offset
        p += 30

def init_sieve(n):
    b = bytearray((n + 30) // 30)
    return b

1voto

Aaron Points 4138

Ceci est vraiment un problème très difficile. Avec un N maximal possible de 10^8, utiliser un octet par valeur résulte en presque 100 Mo de données sans aucun surdébit. Même diviser les données en ne stockant que les nombres impairs vous rapprochera très près de 50 Mo une fois le surdébit pris en compte.

Cela signifie que la solution devra utiliser une ou plusieurs des quelques stratégies suivantes :

  1. Utiliser un type de données plus efficace pour notre tableau de drapeaux de primalité. Les listes Python maintiennent un tableau de pointeurs vers chaque élément de la liste (4 octets chacun sur un Python 64 bits). Nous avons effectivement besoin d'un stockage binaire brut, ce qui ne laisse pratiquement que bytearray dans le Python standard.
  2. Utiliser seulement un bit par valeur dans le crible au lieu d'un octet entier (Bool a techniquement seulement besoin d'un bit, mais utilise généralement un octet entier).
  3. Diviser pour éliminer les nombres pairs, et éventuellement aussi les multiples de 3, 5, 7 etc.
  4. Utiliser un crible segmenté

J'ai initialement essayé de résoudre le problème en ne stockant qu'un bit par valeur dans le crible, et bien que l'utilisation de la mémoire était effectivement conforme aux exigences, la manipulation lente des bits par Python a rendu le temps d'exécution beaucoup trop long. Il était également assez difficile de comprendre l'indexation complexe pour s'assurer que les bons bits étaient comptés de manière fiable.

J'ai ensuite implémenté la solution ne gardant que les nombres impairs en utilisant un bytearray et même si c'était un peu plus rapide, la mémoire posait toujours problème.

Implémentation des nombres impairs par bytearray :

class Sieve:
    def __init__(self, n):
        self.not_prime = bytearray(n+1)
        self.not_prime[0] = self.not_prime[1] = 1
        for i in range(2, int(n**.5)+1):
            if self.not_prime[i] == 0:
                self.not_prime[i*i::i] = [1]*len(self.not_prime[i*i::i])
        self.n_prime = n + 1 - sum(self.not_prime)

    def is_prime(self, n):
        return int(not self.not_prime[n])

def main():
    n, q = map(int, input().split())
    s = Sieve(n)
    print(s.n_prime)
    for _ in range(q):
        i = int(input())
        print(s.is_prime(i))

if __name__ == "__main__":
    main()

Une réduction supplémentaire de la mémoire à partir de cela devrait* le rendre opérationnel.

EDIT : également le retrait des multiples de 2 et 3 ne semblait pas être suffisant en termes de réduction de mémoire même si guppy.hpy().heap() semblait suggérer que mon utilisation était en fait un peu inférieure à 50 Mo.

0voto

Anmol Singh Jaggi Points 3917

Je pense que vous pouvez essayer en utilisant une liste de booléens pour indiquer si son index est premier ou non :

def sieve_of_erato(range_max):
    primes_count = range_max
    is_prime = [True for i in range(range_max + 1)]
    # Cross out all even numbers first.
    for i in range(4, range_max, 2):
        is_prime[i] = False
        primes_count -=1
    i = 3
    while i * i <= range_max:
        if is_prime[i]:
            # Update all multiples of this prime number
            # CAREFUL: Take note of the range args.
            # Reason for i += 2*i instead of i += i:
            # Since p and p*p, both are odd, (p*p + p) will be even,
            # which means that it would have already been marked before
            for multiple in range(i * i, range_max + 1, i * 2):
                is_prime[multiple] = False
                primes_count -= 1
        i += 1

    return primes_count

def main():
    num_primes = sieve_of_erato(100)
    print(num_primes)

if __name__ == "__main__":
    main()

Vous pouvez utiliser le tableau is_prime pour vérifier plus tard si un nombre est premier ou non en vérifiant simplement is_prime[numéro] == True.

Si cela ne fonctionne pas, alors essayez le crible segmenté.

En bonus, vous pourriez être surpris de savoir qu'il existe un moyen de générer le crible en O(n) plutôt qu'en O(nloglogn). Vérifiez le code ici.

0voto

Alain T. Points 1649

Voici un exemple d'approche du crible segmenté qui ne doit pas dépasser 8 Mo de mémoire.

def primeSieve(n,X,window=10**6): 
    primes     = []       # ne stocke que le nombre minimum de nombres premiers pour décaler les fenêtres
    primeCount = 0        # compter les nombres premiers au-delà de ceux stockés
    flags      = list(X)  # les nombres seront remplacés par 0 ou 1 au fur et à mesure
    base       = 1        # nombre correspondant au 1er élément du crible
    isPrime    = [False]+[True]*(window-1) # crible initial

    def flagPrimes(): # marquer les valeurs de x pour la fenêtre actuelle du crible
        flags[:] = [isPrime[x-base]*1 if x in range(base,base+window) else x
                    for x in flags]
    for p in (2,*range(3,n+1,2)):       # nombres premiers potentiels : 2 et nombres impairs
        if p >= base+window:            # décaler la fenêtre du crible si nécessaire
            flagPrimes()                # définir les drapeaux X avant de décaler la fenêtre
            isPrime = [True]*window     # initialiser la prochaine fenêtre du crible
            base    = p                 # 1er nombre dans la fenêtre
            for k in primes:            # mettre à jour le crible en utilisant les nombres premiers connus
                if k>base+window:break
                i = (k-base%k)%k + k*(k==p)  
                isPrime[i::k] = (False for _ in range(i,window,k))
        if not isPrime[p-base]: continue
        primeCount += 1                 # compter les nombres premiers 
        if p*p<=n:primes.append(p)      # stocker les nombres premiers en décalant, mettre à jour le crible
        isPrime[p*p-base::p] = (False for _ in range(p*p-base,window,p))

    flagPrimes() # mettre à jour les drapeaux avec la dernière fenêtre (devrait couvrir le reste d'entre eux)
    return primeCount,flags     

output:

print(*primeSieve(9973,[1,2,3,4,9972,9973]))
# 1229, [0, 1, 1, 0, 0, 1]

print(*primeSieve(10**8,[1,2,3,4,9972,9973,1000331]))
# 5761455 [0, 1, 1, 0, 0, 1, 0]

Vous pouvez jouer avec la taille de la fenêtre pour obtenir le meilleur compromis entre le temps d'exécution et la consommation de mémoire. Le temps d'exécution (sur mon ordinateur portable) reste assez long pour des valeurs élevées de n cependant :

from timeit import timeit
for w in range(3,9):
    t = timeit(lambda:primeSieve(10**8,[],10**w),number=1)
    print(f"fenêtre de 10e{w}:",t)

fenêtre de 10e3: 119.463959956
fenêtre de 10e4: 33.33273301199999
fenêtre de 10e5: 24.153761258999992
fenêtre de 10e6: 24.649398391000005
fenêtre de 10e7: 27.616014667
fenêtre de 10e8: 27.919413531000004

Étrangement, les tailles de fenêtre au-delà de 10^6 donnent de moins bonnes performances. Le point optimal semble être quelque part entre 10^5 et 10^6. Une fenêtre de 10^7 dépasserait de toute façon votre limite de 50 Mo.

0voto

Alain T. Points 1649

J'ai eu une autre idée sur la façon de générer rapidement des nombres premiers de manière efficace en mémoire. Il est basé sur le même concept que le Crible d'Eratosthène mais utilise un dictionnaire pour contenir la prochaine valeur que chaque nombre premier invalidera (c'est-à-dire sauter). Cela ne nécessite que le stockage d'une entrée de dictionnaire par nombre premier jusqu'à la racine carrée de n.

def genPrimes(maxPrime):
    if maxPrime>=2: yield 2           # traitement spécial pour 2
    primeSkips = dict()               # sautValeur : premier
    for n in range(3,maxPrime+1,2):
        if n not in primeSkips:       # si ce n'est pas dans la liste de sauts, c'est un nouveau premier
            yield n
            if n*n <= maxPrime:       # le premier saut sera à n^2
                primeSkips[n*n] = n
            continue
        prime = primeSkips.pop(n)     # trouver le prochain saut pour le premier de n
        saut  = n+2*prime
        while saut in primeSkips:     # ne doit pas déjà être sauté
            saut += 2*prime                
        if saut<=maxPrime:            # ne pas sauter au-delà de maxPrime
            primeSkips[saut]=prime           

En utilisant ceci, la fonction primeSieve peut simplement parcourir les nombres premiers, les compter et marquer les valeurs x:

def primeSieve(n,X):
    primeCount = 0
    nonPrimes  = set(X)
    for prime in genPrimes(n):
        primeCount += 1
        nonPrimes.discard(prime)
    return primeCount,[0 if x in nonPrimes else 1 for x in X]

print(*primeSieve(9973,[1,2,3,4,9972,9973]))
# 1229, [0, 1, 1, 0, 0, 1]

print(*primeSieve(10**8,[1,2,3,4,9972,9973,1000331]))
# 5761455 [0, 1, 1, 0, 0, 1, 0]

Cela s'exécute légèrement plus rapidement que ma réponse précédente et ne consomme que 78 Ko de mémoire pour générer des nombres premiers jusqu'à 10^8 (en 21 secondes).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X