Programmierwettbewerb: Primzahlzerlegung

i_hasser

Grand Admiral Special
Mitglied seit
06.06.2002
Beiträge
18.964
Renomée
85
Standort
IO 0x60
Hi

Ziel ist es für eine sehr große Zahl möglichst schnell alle Darstellungsmöglichkeiten durch a*b zu finden, wo bei a und b eine Primzahl sein muss.

Also zb. 21=3*7

Solche Zerlegungen spielen zb. beim Knacken von Verschlüsselungen eine wichtige Rolle.
Sprachen usw. sind alle erlaubt, sagen wir außer reinem Assembler (inline Assembler ist natürlich auch erlaubt). Betriebssysteme geht auch alles, aber ich denk es wäre erstmal besser wir bleiben bei x86 (-16, -32 und -64 ;)). Lösungen auf anderen Architekturen sind natürlich auch willkommen.

Und damit wir nicht in das Dilemma mit Zahlenlibs kommen denk ich beschränken wir das erstmal auf ausgewählte 64bit Integer und sehen wie die Zeiten dafür aussehen. Zum Testen gibts hier eine Liste mit gültigen Zahlen (die sich auch tatsächlich in a*b für Primzahlen zerlegen lassen):

21
(kommt noch mehr ;))


Viel Spaß!
 
So, ich liefere gleich mal den ersten Beitrag, erstmal unkommentiert. Die Kommentare mach ich grade, wer es also noch nicht versteht, etwas Geduld bitte ;)

Prim.java
Start.java

Das Prinzip dahinter ist ganz simpel: Es wird zunächst ein Feld erzeugt mit allen Primzahlen zw. 0 und 400. Dies ist variabel, so dass diese Lösung grundsätzlich auch für größere Zahlen geeignet ist.
Anschließend wird die gegebene Zahl (in unserem Fall 21) der Reihe nach, von der kleinsten Zahl beginnend durch jede Zahl die in dem Feld gespeichert ist geteilt und festgestellt, ob das Ergebnis auch eine Primzahl ist. Ist dem der Fall, dann haben wir das Ergebnis - fertig.

Edit: So, mittlerweile hab ich auch die Kommentare hinzugefügt, mehr gibt's da eigentlich nicht zu sagen. Für kleine Zahlen ist diese Lösung sehr schnell, mal sehen was ich ändern muss wenn größere Zahlen ins Spiel kommen. Natürlich ist diese Lösung as-is höchstens für Zahlen des Datentyps long zulässig.
 
ich werde die tage mal probieren unseren lenstra mit elliptischen kurven algo von maple auf c zu porten.

evtl vorher auch die pollard p-1 methode, weil die wesentliche einfacher zu implementieren ist.
 
Ich denk, ich werd mich heut Abend mal dran machen, das Sieb des Eratosthenes in Java implementieren. Das sollte die Erstellung des Arrays mit Primzahlen stark beschleunigen und auch für größere Zahlen fitmachen ;)

@[ab]noname:
Dein Code compiliert nicht.
 
Compiler gcc 2.95.4 unter Debian Linux. Funktioniert im übrigen auch nicht, wenn ich __int64 durch long ersetzt im typedef. Die genaue Fehlermeldung:

martin@debian:~/sw$ g++ prime.cpp -o prime
prime.cpp: In function `void Search2PrimFacts(long int)':
prime.cpp:23: implicit declaration of function `int printf(...)'

Ich installiere grade gcc-3.3, mal sehen ob's damit geht.
 
Nope, auch ein

#include <cstdlib>

hilft nicht, gleicher Fehler.
 
So, jetzt hab ich das ganze mittels BigInteger implementiert. Sieht jetzt zwar deutlich leichter und strukturierter aus, ist aber langsamer. Das liegt nicht nur daran, dass
Java's BigInteger relativ lahm ist (Strukturbedingt) sondern auch daran, dass eine Implementierung eines Primzahlsiebs nicht nur unnötig kompliziert, sondern auch unnötig geworden ist.

Der Quellcode kann hier gefunden werden, derzeit läuft grade ein Testlauf auf einem Pentium 4 / 1.8 GHz und Debian Linux mit der kleinsten der von JKuehl geposteten Zahlen.

Edit: HAHA! Im Moment in dem ich es schreibe, bringt er auch schon ein Ergebnis, ich werd jetzt auch mal die anderen Zahlen durchlaufen lassen. An der nächstgrößeren Zahl rechnet er jetzt schon eine Ewigkeit rum...



Ergebnisse Intel Pentium4 (1.8 GHz):

outdated, siehe unten


Ergebnisse AMD Opteron 144 (1.8 GHz):

outdated, siehe unten
 
Mal sehen ob ich morgen Zeit hab (im Moment hab ich leider garkeine :() - dann werd ich euch mit meinem C-Hickhack beglücken *chatt*
 
Hmm... der zweite Durchlauf läuft immer noch, mittlerweile schon gut über 40 Minuten :-/
Ich glaub, da ist noch einiges an Optimierungsarbeit zu leisten. Werd's trotzdem mal über Nacht auf'm Opteron laufen lassen, allerdings nicht die zweitkleinste sondern gleich die größte der von JKuehl genannten Zahlen *buck*

Edit:
HOLY F***
Vergesst die Ergebnisse oben, durch minimales Optimieren bin ich jetzt auf folgendes Ergebnis gekommen (gemessen auf einem AMD Opteron 144 (1.8 GHz) mit 1024MB DDR333 Speicher unter Debian Linux, Java 1.4.2):

1949565409282837: 0m6.785s
312133288776418481: 2m11.723s
11081452861688528621: 20m48.564s

Der optimierte Quellcode findet sich hier. Und für diejenigen, die Probleme habe das ganze mit javac zu compilieren, hab ich auch gleich noch die .class Dateien beigefügt.
 
Tagchen,

wo ich gerade das hier sehe:

for(i64 i = 3; i <= Number; i += 2)

Ihr braucht übrigens nur soviel hier zu testen:

for(i64 i = 3; i <= sqrt(Number); i += 2)

weil mathematisch beweisbar:

sqrt(pq) > p oder sqrt(pq) > q

heißt im Prinzip, einer der beiden Primfaktoren ist auf jedenfall kleiner als die Wurzel des Produkts.

Ich würde also sowas machen (für max. zwei Primfaktoren):

Code:
void Search2PrimFacts(i64 Number) {
  i64 i = 3; // naja, die zwei ist ja fast wirklich egal
  bool found = false;

  while ( i <= round(sqrt(Number)) && !found)
  {
     if (isPrime(i))
     {
        if (Number % i == 0)
        {
           printf("%I64d*%I64d=%i64d\n", i, Number / i, Number);
           found = true;
        }
     }
     i += 2;
  }
}

m.f.g.
BoMbY
 
Zuletzt bearbeitet:
Wobei ich gerade Überlege: Ist die statistische Wahrscheinlichkeit nicht höher das die Gesuchte Primzahl p irgendwo zwischen sqrt(pq)/2 und sqrt(pq) liegt, als zwischen 3 und sqrt(pq)/2? Wenn dem so ist würde ich nicht von 3 hochzählen sondern von sqrt(pq) runter. Wobei, je höher die getestete Zahl, desto länger dauert es ja auch das Modulo zu berechnen...

Das müsste ich mal genau ausrechnen/testen.

m.f.g.
BoMbY
 
Also wenn ich nicht ganz dumm bin, ist das hier der Beweis (V=oder):

Annahme: sqrt(pq) > p V sqrt(pq) > q

Zwischenschritt: pq > p² V pq > q²

Schluss: q > p V p > q

Sprich also q ist größer p oder p ist größer als q <- und davon können wir ja wohl ausgehen, oder? Okay, ich war jetzt zu faul den Fall (gleich =) zu berücksichtigen, aber der wird auch abgedeckt.

m.f.g.
BoMbY

Edit: PS: Klar, das ist nicht schneller wenn man einen Primfaktor findet, nur wenn man keinen findet!
 
Original geschrieben von [ab]noname
Das zählen bis zur Quadratwurzel ist eigentlich schon in meiner IsPrime Funktion integriert und vergleich auch nicht das Quadrat, sondern das Produkt, da Wurzelziehen langsamer ist, als ein Produkt zu berechnen:
- i*i <= Number ist äquivalent zu i <= sqrt(Number), jedoch schneller!
Sicher ist es schneller zu quadrieren, als die Wurzel zu ziehen. Aber da Number in der while-Schleife nicht verändert wird, braucht die Berechnung nur einmal pro Aufruf der Funktion Search2PrimFacts() durchgeführt zu werden. Dann sollte sich das wieder relativieren, es sei denn Du rufst besagte Funktion sehr oft (!) auf.
Theoretisch sollte der Compiler den Quellcode dahingehend optimieren, daß die angesprochene Berechnung auch nur einmal durchgeführt wird, aber zur Sicherheit solltest Du das mal austesten, indem Du round(sqrt(Number)) vor der while-Schleife berechnest.

[EDIT]
Und die Boolsche Variable brauchst Du auch nicht. Einfach mit break die Schleife beenden. Dadurch sparst Du pro Durchlauf einen Test (!found).
Code:
void Search2PrimFacts(i64 Number) {
  i64 i = 3, // naja, die zwei ist ja fast wirklich egal
      tmp; 
  
  tmp = round(sqrt(Number));

  while ( i <= tmp)
  {
     if (isPrime(i))
     {
        if (Number % i == 0)
        {
           printf("%I64d*%I64d=%i64d\n", i, Number / i, Number);
           [b]break; [/b]
        }
     }
     i += 2;
  }
}
[/EDIT]
 
Nur mal so ne generelle Frage,
brecht ihr nach der ersten Möglichkeit ab, oder lasst
ihr alle Möglichkeiten ausrechnen ???
Wenn ich aber Zeiten von 6s sehe, denke ich, dass nach der
1. gefundenen Möglichkeit abgebrochen wird.
Interessant wird IMHO aber erst wenn man alle sucht,
das dauert dann wirklich mal nen paar Stunden für so Zahlen wie oben genannt.
 
Original geschrieben von [ab]noname
In der Funktion IsPrime sollte das schon einen Geschwindigkeitsvorteil bringen. In der äußeren Schleife hast du dagegen Recht.
Hmm .. ich habe gerade Deinen Sourcecode ein paar Postings weiter oben entdeckt. Da hast Du das ja schon ganz anders geregelt. Jetzt sehe ich Deinen vorherigen Kommentar in einem ganz anderen Lichte.
Ich lasse mal meine grauen Zellen ackern, was da eventuell noch zu optimieren ist ... *kopfkratz

[EDIT]
Code:
for(i64 d = 2; d*d <= nr+1; d++)
Das schreit geradezu danach, optimiert zu werden! Vergleich doch nicht das Quadrat von d mit nr+1, sondern d mit der Wurzel aus nr+1 oder was auch immer in Frage käme ... so genau habe ich nun nicht darüber nachgedacht. Die Wurzel natürlich nur 1x vor der Schleife berechnen! So sparst Du irre viele Quadrierungen!
[/EDIT]
 
Zuletzt bearbeitet:
Original geschrieben von [ab]noname
Selbst n-faches Quadrieren ist bei Zahlen dieser Größenordnung schneller als die Wurzel ziehen.
Da ist doch was faul ... nämlich der Compiler. Wie ich oben schon schrieb, führe die Berechnung der Wurzel doch mal vor der Schleife aus. Ich habe das ungute Gefühl, das für jeden Durchlauf der while-Schleife die Wurzelberechnung beim Test der Schleifenbedingungen erneut ausgeführt wird, was natürlich ein Performancefresser ist!

Code:
bool IsPrime(i64 nr) {
  i64 tmp = ((i64)sqrt(nr))+1;
  
  for(i64 d = 2; d <= tmp; d++) {
    if (!(nr % d)) { 
      return false;
    }       
  }
  return true;
}
Benchmarke doch bitte mal mit obiger Funktion.

Und die Funktion sollte doch sicherlich bool und nicht long zurückgeben?
 
Original geschrieben von [ab]noname
D'Espice liest erstmal die Primzahlen in ein Array so wie ich das verstanden habe - berechnen muss man sie aber dennoch. Bei mir wird halt pro Primzahl getestet, wie es sich mit dem 2. Faktor der bei einer Division entsteht verhält. - kein Plan, warum das so hinkt :]
Das war die erste Fassung, ja. Hat Vorteile, wenn man mehrere Zahlen hintereinander testen will weil dann nur einmal das Array durchlaufen wird was mit einer Komplexität von O(n) geschieht.
Die zweite Version, auf BigInteger basierend ist bei einzelnen Zahlen viel schneller da erst getestet wird, ob die Zahl modulo Schleifenzähler gleich Null ist. Ist dem nicht so, wird der Schleifenzähler erhöht und weitergemacht, ist dem so wird getestet, ob der Schleifenzähler eine Primzahl ist (BigInteger.isProbablePrime()) und ist auch das erfüllt, dann wird geschaut ob die Zahl geteilt durch Schleifenzähler auch eine Primzahl ist - wenn das alles zutrifft (schön der Reihe nach), dann haben wir ein Ergebnis.
 
Original geschrieben von [ab]noname
Hab die Wurzel ausgelagert, 330s... Das optimiert auch jeder anständige Compiler.

Ich bin fest der Überzeugung, dass ein FP Sqrt viel zu viel Zeit braucht, als ne (vergleichsweise) simple Multiplikation, gleich, dass sie vielfach vorkommt.

Habs auch erst nicht geglaubt, aber die Code zeigts doch? :]
Jaja, okay. Ich seh's ja ein. Hatte aus den Augen verloren, daß isPrime() andauernd aufgerufen wird. *kippt Asche über's eigene Haupt* Da es sich um eine Integer-Wurzel handelt, muß es doch haufenweise Optimierungen für eine solche Berechnung geben! Vielleicht sogar mit Hilfe von MMX. Was sagen denn die AMD bzw. Intel TechDocs? Steht da zufällig schon eine Musterlösung drin?
 
Naja nein, nicht wirklich. Das ziehen einer Wurzel ist einfach extrem aufwändig.

@[ab]noname:
Für welche Zahl braucht dein Algorithmus 330s? Die erste der von JKuehl genannten? Wenn ja, dann musst du noch sehr viel optimieren, ich bin da mit Java bereits bei unter 7s angekommen ;)
Du kannst deinen Algorithmus extrem beschleunigen, indem du die Reihenfolge bei der Abfrage umdrehst. Du prüfst derzeit zunächst für jede beliebige Zahl ob sie Prim ist und erst dann, ob sie Modulo Null ergibt - dreh das um! Es reicht wenn du nur die Zahlen auf prim prüfst, die wirklich Modulo gerechnet auch Null ergeben. Das spart dir unglaublich viel Zeit ein, du wirst erstaunt sein.
 
Original geschrieben von [ab]noname
Hrhr der Modulo Trick ;D

Jetzt hab ichs auf t = 0 bis 1sec für die 1. Zahl - Yippieeh! thx D'Espice
Na siehste, geht doch ;D
Jetzt muss ich wieder kontern, meine 7s wurden unterboten. Leider wird das wohl kaum möglich sein, da BigInteger einfach extrem lahm ist. Aber ich werd heut Nacht überlegen, ob sich da noch was drehen und optimieren lässt.
Was ich mir überlegt habe ist ein Feld anzulegen, welches alle Zahlen enthält die bereits als negativ geprüft wurden. Für jede neue Zahl kann dann zunächst geprüft werden, ob sie ein Vielfaches einer der bereits gespeicherten Zahlen ist. So kann man sich nochmals einige hundertausend Rechnungen sparen.
 
Unter Linux kann ich das genau messen, ich werd gleich versuchen ob ich deinen Quellcode mit gcc compilieren kann und wenn ja, sag ich dir die genaue Zeit auf dem Opteron für den Algorithmus ;)

EDIT:
Mir fällt grad ein, wird eh nix bringen. Denn gcc kann mit __int64 nix anfangen und long ist zu klein für diese Zahl. Vergessen wir die Linux-Idee also gleich wieder ;)
 
Original geschrieben von D'Espice
EDIT:
Mir fällt grad ein, wird eh nix bringen. Denn gcc kann mit __int64 nix anfangen und long ist zu klein für diese Zahl. Vergessen wir die Linux-Idee also gleich wieder ;)
Kein Problem:

Code:
[b]long long[/b] number;
[...]
Search2PrimFacts(1949565409282837[b]LL[/b]);

@GMP-Library:
Das ist ganz easy und die Library ist auch schön schnell. :)
 
So, hier ist meine Version mit der GMP-Library. Da ist noch nix entsprechend optimiert. Es müßte noch die Anzahl der Aufrufe der mpz_init()-Funktion in der IsPrime()-Funktion drastisch verringert werden und was weiß ich noch alles, um mehr Performance zu erreichen, aber dafür habe ich leider keine Zeit mehr.
Code:
#include <stdio.h>
#include <stdbool.h>
#include <time.h>
#include <math.h>
#include <gmp.h>

bool IsPrime(mpz_t nr) {
	mpz_t d, tmp;
	
	mpz_init(d);
	mpz_init(tmp);
	mpz_set_ui(d, 2);
	mpz_pow_ui(tmp, d, 2);
	//printf("IsPrime\n");
	while(mpz_cmp(tmp, nr) <= 0) {
		//gmp_printf("d=%Zd, tmp=%Zd\n", d, tmp);
		if(mpz_divisible_p(nr, d))
			return false;
		mpz_add_ui(d, d, 1);
		mpz_pow_ui(tmp, d, 2);
	}
	return true;
}

void Search2PrimeFacts(mpz_t number) {
	mpz_t a, i, tmp;
	
	mpz_init(a);
	mpz_init(i);
	mpz_init(tmp);
	mpz_set_ui(i, 3);
	mpz_sqrt(tmp, number);
	
	while(mpz_cmp(i, tmp) <= 0) {
		if(mpz_divisible_p(number, i)) {
			if(IsPrime(i)) {
				mpz_cdiv_q(a, number, i);
				if(IsPrime(a)) {
					gmp_printf("%Zd * %Zd = %Zd\n", a, i, number); 
					break;
				}
			}
		}
		mpz_add_ui(i, i, 2);
	}
}

int main(void) {
	float millisecs;
	clock_t start, end;
	
	mpz_t number;
	
	mpz_init(number);
	
	mpz_set_str(number, "312133288776418481", 10);
	
	start = clock();     
	Search2PrimeFacts(number);
	end = clock();
	millisecs = (float)(end - start)/((float)CLOCKS_PER_SEC/1000);
	printf("Elapsed time in milliseconds: %f\n", millisecs);
	
	return 0;
}
Ein paar print-Befehle zum Debuggen habe ich mal nicht gelöscht.

Viel Spaß damit. ;)

[EDIT]
Ich habe mal die Search2PrimeFacts() Funktion leicht abgewandelt in Anlehnung an BoMbY's Beweis. Die Berechnungsdauer für "312133288776418481" wurde um etwas mehr als die Hälfte gesenkt.
[/EDIT]
 
Zuletzt bearbeitet:
Ich hoffe, es ist bekannt, daß Primzahltest in NP (und sogar coNP) liegt und die gängigen Alg auch nicht darüberhinaus kommen? Ein Bekannter hat mir zwar gesagt, daß es mittlerweile einen O(N) Algorithmus gibt, aber der soll wohl erst bei Zahlen mit einigen Dutzend (oder waren es hundert?) Nullen, schneller sein, als die bisher bekannten...aufgrund einer groooooßen Konstate vor dem "n".

Ist es nicht stupide, irgendwelche Algorithmen zu implementieren, ohne sich schlau zu machen, was es für Alg gibt bzw. ohne großartig Theorie zu verstehen, dumm draufloszucoden? Etwa das Mersenne Prime Projekt liefert interessante und vor allem schnelle Algorithmen....da zu optimieren wäre interessanter und auch von bleibenden Wert.

Naja, mehr als Beschäftigungstherapie wird das so nicht werden... :]
 
Zurück
Oben Unten