C++ - Ricerca numeri primi

Pubblicità
Magari se posti il codice in questione possiamo darci un'occhiata.
non l'ho mantenuto ma sicuramente avrò pasticciato con gli indici i e j, infatti adesso
C++:
for(int j = i*i; j<dim; j += 2*i) // crivello
    p[j] = 0;
ritorna i risultati giusti, quindi devo aver corretto tutto ma avevo lasciato il ciclo più inefficiente (in termini di millisecondi misurati non è cambiato praticamente nulla)
durante le ultime prove ho pure scoperto di non potermi fidare più del compilatore GCC che uso, eppure è il 13.2.0 , non è l'ultima (sub)versione ma pensavo andasse bene, invece su input >16,2 milioni il .exe compilato esce con un errore incomprensibile e lo fa apparentemente a caso (me ne sono accorto variando l'intervallo del bitset)
Per le ultime verifiche sui 2 cicli mi sono affidato a un compilatore online: https://www.onlinegdb.com/
i risultati coincidono con quelli di Mahtematica, per cui il codice, piccole inefficienze a parte, è corretto.

Per il resto concordo, usando long invece di int sugli indici evitere anche l'uso di radice

Al momento sono soddisfatto, penso che cercherò di procurarmi qualche testo di teoria dei numeri, mi è sempre interessata ma non ho mai approfondito come si dovrebbe.
 
durante le ultime prove ho pure scoperto di non potermi fidare più del compilatore GCC che uso, eppure è il 13.2.0 , non è l'ultima (sub)versione ma pensavo andasse bene, invece su input >16,2 milioni il .exe compilato esce con un errore incomprensibile e lo fa apparentemente a caso (me ne sono accorto variando l'intervallo del bitset)
Hai provato a dichiarare il bitset static (o equivalentemente come variabile globale) come ti ho suggerito in precedenza?



Intanto ho aggiornato il programma apportando alcune correzioni e modifiche:

C++:
#include <iostream>
#include <bitset>
#include <cstdint>
#include <vector>

using namespace std;

void espandi_e_applica_crivello(vector<uint32_t> &v, uint32_t &last)
{
    static const uint32_t N = 51'130'563;
    static bitset<N + 1> b;
    uint32_t first = last + 2;
    uint64_t n, q;
    uint32_t dim = N + (v.size() == 1);
    last += 2 * dim;
    if(v.size() == 1)
    {
        for(uint32_t i = 0; ; ++i)
        {
            if(!b[i])
            {
                n = 2 * i + first;
                if((q = n * n) > last)
                {
                    break;
                }
                for(uint32_t j = (q - first) / 2; j < dim; b.set(j), j += n);
            }
        }
    }
    else
    {
        b.reset();
        for(uint32_t i = 1, j; (q = (n = v[i]) * v[i]) <= last ; ++i)
        {
            if(q < first)
            {
                uint32_t r = first % n;
                j = r ? (r % 2 ? (n - r) / 2 : n - r / 2) : 0;
            }
            else
            {
                j = (q - first) / 2;
            }
            for(; j < dim; b.set(j), j += n);
        }
    }
    for(uint32_t i = 0; i < dim; ++i)
    {
        if(!b[i])
        {
            v.push_back(2 * i + first);
        }
    }
}

uint32_t next_prime_number(const uint32_t n)
{
    if(n < 2)
    {
        return 2;
    }
    static vector<uint32_t> v = {2};
    static uint32_t last = 1;
    while(n >= v.back() && last != UINT32_MAX)
    {
        espandi_e_applica_crivello(v, last);
    }
    cout << "\n " << v.size() << " NUMERI PRIMI <= " << last << " (" << (double)v.size() / last * 100 << "%)\n";
    if(n >= v.back())
    {
        return 0;
    }
    uint32_t i_dx = v.size() - 1;
    for(uint32_t i_sx = 0, i; i_dx != i_sx + 1; (n < v[i = (i_sx + i_dx) / 2] ? i_dx : i_sx) = i);
    return v[i_dx];
}

int main()
{
    uint32_t n = 318'912'486;

    uint32_t p = next_prime_number(n);
    cout << "\n NEXT PRIME(" << n << ") = " << p << endl;
}

- giusto per ricapitolare, il programma implementa la funzione next_prime_number(n) sfruttando un crivello di Eratostene espandibile. Il risultato viene ricercato nell'ambito dei 32 bit, e se non esiste, la funzione ritorna 0 (come nel caso di next_prime_number(UINT32_MAX) );

- per la precisione i numeri primi trovati ad ogni espansione del crivello vengono salvati in un vettore v e la funzione espandi_e_applica_crivello() viene richiamata finché n risulta maggiore o uguale all'ultimo elemento del suddetto vettore (e ovviamente fino a quando l'ultimo valore vagliato last risulta inferiore a UINT32_MAX , che come sappiamo è pari a 2^32-1) ;

- la risposta viene ricercata all'interno di v mediante una ricerca binaria (o almeno credo che si chiami così, l'algoritmo l'ho scritto ragionando sul momento);

- se il crivello viene applicato almeno una volta, oltre alla risposta il programma stampa anche una statistica circa il numero di primi minori o uguali al massimo valore vagliato;

- infine penso sia doveroso spiegare perché ho fissato N = 51.130.563 e la dimensione del bitset a N+1 🙂! Al fine di coprire tutti gli interi a 32 bit è essenziale che l'ultimo valore vagliato dall'ultima chiamata a espandi_e_applica_crivello() sia proprio uguale a UINT32_MAX . I valori da vagliare sono i numeri dispari che potenzialmente vanno da 3 a 2^32-1, per un totale quindi di ((2^32-1)-1)/2=2.147.483.647 valori. Il problema è che il suddetto valore è un numero primo, e quindi non può essere scomposto in parti uguali. Quindi ho preso in considerazione il valore precedente 2147483646=2×3^2×7×11×31×151×331 e ho fissato N combinando i precedenti fattori al fine di ottenere un valore prossimo ai 50 milioni (che era il valore a cui avevo settato N nella precedente versione del programma), e per la precisione ho impostato N=3×11×31×151×331=51.130.563 (restano fuori i fattori 2×3×7=42). A questo punto considerando che

2.147.483.647=42×51.130.563+1=51.130.564+41×51.130.563

ho settato la dimensione del bitset a 51.130.564, andando poi nella prima espansione a considerare tutti i 51.130.564 elementi, mentre nelle potenziali 41 espansioni successive alla prima vado a considerare solo i primi 51.130.563 elementi del bitset.


Ovviamente se riscontrate qualche errore fatemi sapere.
 
Cavolo devo riuscire a leggervi con calma e non da smartphone magari...

Vedo che state progredendo parecchio. Complimenti a entrambi (anche per la voglia). 😁
 
Hai provato a dichiarare il bitset static (o equivalentemente come variabile globale) come ti ho suggerito in precedenza?
no, mi premeva aggiustare il codice scritto
però sì, in questo modo funziona, basta scegliere una dimensione corretta buona all'uso e poi usarlo sia per inizializzazione che per successive espansioni.
A me in generale premono di più gli algoritmi generali, meno l'implementazione in uno specifico linguaggio: in questo caso avevo pensato alle sequenze di bit perché permettono un grosso risparmio di RAM ed in particolare perché il C/C++ permettono l'accesso diretto in lettura/scrittura ai singoli bit, quindi una velocità altissima di elaborazione.
Per i prossimi problemi userò Java, in questo caso era teoricamente possibile ma sarebbe stato peggiorativo: Java ha i bitset ma ciascun elemento è rappresentato da un boolean, però per i boolean si usa 1 byte e già questo vanificava il tutto; come se non bastasse ho letto che l'implementazione effettiva usa internamente un long (64 bit) 😅 invece di rispariare RAM la sprecavo!
- se il crivello viene applicato almeno una volta, oltre alla risposta il programma stampa anche una statistica circa il numero di primi minori o uguali al massimo valore vagliato;
solo questo cambierei, rispettando le regole della programmazione a oggetti:
  • la funzione deve restituire solo il valore desiderato (a al limite n valore convenzionale, per es. -1,, se il risultato non rientra nel rangge): sarà compito di chi richiama/uso la funzione analizzare il risultato ed eventualmente stampare un messaggio
  • per la statistica una funzione a parte void che prende in ingersso un vector e, in base alla sua dimensione, stampa le informazioni; ovviamente il vector va definito fuori da next_prime_number, altrimenti non si può fare
queste 2 cose sono "finezze" che comunque non hanno nulla a che fare con l'algoritmo
se riscontrate qualche errore fatemi sapere
non ci sono errori, quando si implementa il programma si fanno delle scelte ponderate e se fai un certo tipo di ragionamento basta che sia coerente. Figurati chela mia idea era di fissare un bitset di inizializzazione molto più piccolo (2^16=65536).

Però conviene fare un ragionamento differente sull'uso di next_prime_number:
se hai deciso che N sia 50 milioni e spiccioli, è conveniente creare direttamente il vector che contiene tali primi, perché la funzione next_prime_number lo farà in ogni caso, quindi tanto vale precalcolrsela una volta per tutte e fare le espansioni avendolo già come base;
tale vector sarà una "costante" (o variabile statica o comunque si faccia in C++) e sarebbe un piccolo database di primi da calcolare una tantum su cui poggeranno le successive chiamate di next_prime_number (che ripeto, in sua assenza dovrebbe comunque calcolare ogni volta).
Vedo che state progredendo parecchio
si ragiona per gioco più che altro: stima maneggiando numeri piccoli, ma la matematica non lascia scampo, possiamo ottimizzare fino all'inverosimile ma funzionerà sono per piccoli intervallli, che però vanno bene a scopo didattico e sono un buon esercizio di programmazione tutto sommato, sempre meglio che istupidirsi davanti ad uno smartphone no? 😆
 
Però conviene fare un ragionamento differente sull'uso di next_prime_number:
se hai deciso che N sia 50 milioni e spiccioli, è conveniente creare direttamente il vector che contiene tali primi, perché la funzione next_prime_number lo farà in ogni caso, quindi tanto vale precalcolrsela una volta per tutte e fare le espansioni avendolo già come base;
tale vector sarà una "costante" (o variabile statica o comunque si faccia in C++) e sarebbe un piccolo database di primi da calcolare una tantum su cui poggeranno le successive chiamate di next_prime_number (che ripeto, in sua assenza dovrebbe comunque calcolare ogni volta).
v e last sono variabili statiche, quindi successive chiamate a next_prime_number(n) richiameranno la funzione espandi_e_applica_crivello()
solo se necessario, ossia se n risulta maggiore o uguale all'ultimo elemento di v ! 🙂


Comunque fissando nel main n=UINT32_MAX il programma setaccerà tutti gli interi a 32 bit (a tal proposito il mio catorcio lo fa in poco meno di un minuto, sarebbe interessante vedere i tempi su un PC più performante), il che consente poi anche di implementare una funzione is_prime_number() molto efficiente su tutti gli interi a 64 bit (se il numero è minore di 2^32 basta una ricerca binaria nel vettore dei primi, in caso contrario basta testare il numero per i primi minori o uguali alla sua radice quadrata intera).
A proposito di radice intera, ho intenzione di scrivere una funzione che implementi in binario l'algoritmo che si utilizza per trovare la radice quadrata con carta e penna (non so chi se lo ricorda, somiglia un po' ad una divisione in colonna).
 
v e last sono variabili statiche
cioè in C++ si possono dichiarare variabili statiche in una funzione e rimangono accessibili? allora sì il concetto è quello
se rimangono disponibili pure per altre funzioni ancora meglio
non conosco il C++, sarebbe da rendere il vector accessibie anche ad alrre funzioni, potrebbe essere utile, per esempio immagina di scrivere un programma che fa la decomposizione i fattori primi, se hai il vector fuori è un vantaggio
se è già così come non detto
il mio catorcio lo fa in poco meno di un minuto
che PC è?
se mi passi il .exe, lo zippi e lo metti su un server di condivisione file, e mi passi il link te lo provo, anche il mio Pc fa abbastanza schifo ma vediamo
per trovare la radice quadrata con carta e penna (non so chi se lo ricorda, somiglia un po' ad una divisione in colonna).
il metodo per farlo a mano non me lo ricordo, è macchinoso
con un ciclo che prova dimezzando ogni volta l'intervallo lo fai in poche istruzioni, basta che eviti il solito overlflow quando inevitabilmente moltiplichi i*i!, sempre che stiamo parlando di radici intere
se invece vuoi proprio replicare il metodo su carta e penna, divertiti 😆
 
cioè in C++ si possono dichiarare variabili statiche in una funzione e rimangono accessibili? allora sì il concetto è quello
se rimangono disponibili pure per altre funzioni ancora meglio
Esatto, una variabile statica viene inizializzata una sola volta (di default a zero in relazione al tipo) e rimane disponibile per tutta la durata del programma. Per quanto riguarda la visibilità invece, essa risulta utilizzabile solo nell'ambito (nella funzione in questo caso) in cui viene definita.

che PC è?
se mi passi il .exe, lo zippi e lo metti su un server di condivisione file, e mi passi il link te lo provo, anche il mio Pc fa abbastanza schifo ma vediamo
Quello per cui mi prendevate in giro in un altro topic! 😁
Comunque non si fa prima a settare nel main n=UINT32_MAX per poi compilare il codice sul proprio pc?!

il metodo per farlo a mano non me lo ricordo, è macchinoso
con un ciclo che prova dimezzando ogni volta l'intervallo credo che lo fai in 3 istruzioni, basta che eviti il solito overlflow quando inevitabilmente moltiplichi i*i!, sempre che stiamo parlando di radici intere
se invece vuoi proprio replicare il metodo su carta e penna, divertiti 😆
Quando lo implemento proviamo a confrontarlo con il metodo di ricerca che hai proposto! 😂
 
Comunque non si fa prima a settare nel main n=UINT32_MAX per poi compilare il codice sul proprio pc?!
no, a parte il fatto che ho dubbi sul compilatore che ho, serve esattamente lo stesso .exe perché i risultati siano confrontabili
se sul tuo, che io non ricordo cosa sia, lo fai in meno di un minuto, vediamo che esce fuori con un altro
Quando lo implemento proviamo a confrontarlo con il metodo di ricerca che hai proposto!
occhio che quello che ho proposto vale solo per gli itneri, se tu vuoi calcolare la radice decimale non vale più
 
no, a parte il fatto che ho dubbi sul compilatore che ho, serve esattamente lo stesso .exe perché i risultati siano confrontabili
se sul tuo, che io non ricordo cosa sia, lo fai in meno di un minuto, vediamo che esce fuori con un altro
A parte il fatto che non so quanto sia portabile un exe creato in C++, poi mi accontenterei di un confronto alla buona (basta che le ottimizzazioni -O2 o -O3 siano attivate)! 😅

occhio che quello che ho proposto vale solo per gli itneri, se tu vuoi calcolare la radice devimale non vale più
Sì sì lo so, d'altronde quello che serve in relazione alla ricerca dei numeri primi è proprio la radice intera di interi.
 
non so quanto sia portabile un exe creato in C++
se hai usato Windows funziona di sicuro, le ottimizzazioni di cui parli non so cosa siano e neanche come attivarle

EDIT: ho provato il copia-incolla del codice, compilato eseguito (nessuna ottimizzazione attivata a meno che non fosse predefinita, non lo so se cambia qualcosa) il risultato è questo
21792607 NUMERI PRIMI <= 409044507 (5.32769%)

NEXT PRIME(318912486) = 318912497

Process returned 0 (0x0) execution time : 9.071 s
Press any key to continue.
9 secondi, considerando che il mio PC fa schifo se sul tuo ci mette un minuto suppongo che hai un Intel Core 2 Duo al massimo, che aspetti a cambiare quella carcassa? 😆 fra poco più di un mese escono i nuovi Intel...

Il risultato però si vede solo se lo lancio dall'IDE (CodeBlocks 20.03 + GCC 13.2.0), dovresti aggiungere nel codice direttamente l'ora di inizio/fine così si vede sempre
 
ho provato il copia-incolla del codice, compilato eseguito
Sì, ma io avevo scritto
Comunque fissando nel main n=UINT32_MAX il programma setaccerà tutti gli interi a 32 bit (a tal proposito il mio catorcio lo fa in poco meno di un minuto, sarebbe interessante vedere i tempi su un PC più performante)
e anche
Comunque non si fa prima a settare nel main n=UINT32_MAX per poi compilare il codice sul proprio pc?!
mentre tu hai lasciato n=318'912'486 , quindi non verranno setacciati tutti i valori minori di 2^32 ! 😅


9 secondi, considerando che il mio PC fa schifo se sul tuo ci mette un minuto suppongo che hai un Intel Core 2 Duo al massimo, che aspetti a cambiare quella carcassa? 😆
Ci hai quasi preso, trattasi di pentium dual core! 😅
Comunque con quell'input impiego 5 secondi scarsi, quindi mi sa che hai le ottimizzazioni disabilitate. Per abilitarle basta andare in
Settings->Compiler...->Compiler Flags e scendere fino a Optimization per attivare il flag -O2 o -O3.
 
Per abilitarle basta andare in
Settings->Compiler...->Compiler Flags e scendere fino a Optimization per attivare il flag -O2 o -O3.
fatto, alla prima run non è cambiato nulla (settato O2), evidentemente l'IDE non aveva recepito, ho dovuto chiudere, cancellare a mano i file .o e .exe, e ricompilare, ora il tempo è 1 secondo e spiccioli (col vecchio limite di n)
tu hai lasciato n=318'912'486 , quindi non verranno setacciati tutti i valori minori di 2^32 !
eh, sto senza connessione mi appoggio allo smartphone per pochi minuti
settando nel main n=UINT32_MAX il tempo è di circa 12 secondi, coerente per le 2 run coi 2 valori di n (5 volte in meno del tuo)
Ci hai quasi preso, trattasi di pentium dual core
scherzi a parte dovresti cambiarlo, è veramente troppo vecchio, ti fa sprecare tempo

prima che me ne dimentico ti segnalo questo sito: https://projecteuler.net/ che contiene una enorme mole di problemini che, ne sono sicurissimo, ti divertirai un mondo a risolvere. La lista la trovi clickando il link Archives (https://projecteuler.net/archives)
la difficoltà dei problemi è grosso modo crescente, anche se non necessariamente, come si vede dalla colonna che conteggia in quanti lo hanno risolto
 
settando nel main n=UINT32_MAX il tempo è di circa 12 secondi, coerente per le 2 run coi 2 valori di n (5 volte in meno del tuo)
Mi fa piacere per il codice che ho scritto, anche se il mio PC ne esce davvero umiliato! 😅

prima che me ne dimentico ti segnalo questo sito: https://projecteuler.net/ che contiene una enorme mole di problemini che, ne sono sicurissimo, ti divertirai un mondo a risolvere. La lista la trovi clickando il link Archives (https://projecteuler.net/archives)
la difficoltà dei problemi è grosso modo crescente, anche se non necessariamente, come si vede dalla colonna che conteggia in quanti lo hanno risolto
Grazie, gli darò sicuramente un'occhiata! 😃
 
anche se il mio PC ne esce davvero umiliato!
ti dirò:
avere un PC lento può perfino fare comodo! quando il PC è lento è possibile vedere "ad occhio" (perché si aspetta) miglioramenti effettivi dell'algoritmo usato; questi miglioramenti su un PC molto veloce non si vedono, ricorda sempre che il tempo fisico (quello che misuri in secondi/millisecondi) non è una misura oggettiva del tempo di esecuzione, un PC velocissimo potrebbe nascondere inefficienze dell'algoritmo che invece potrebbero essere evidenti con un PC dinosauro

infatti per adesso il flag O2 l'ho di nuovo disattivato
 
Pubblicità
Pubblicità
Indietro
Top