Laufzeitverhalten der Primfaktorzerlegung
Beispiel: $$2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, ...$$
Eine der wichtigsten Eigenschaften von Primzahlen ist, dass sie als Bausteine der natürlichen Zahlen angesehen werden können.
Beispiel: Die Zahl 260 kann wie folgt mit Primzahlen aufgebaut werden:
$$260 = 2\cdot 2\cdot 5\cdot 13 = 2^2\cdot 5^1\cdot 13^1$$
Man nennt die Primzahlen, die in einer Produktdarstellung einer gegebenen Zahl vorkommen, auch Primfaktoren der Zahl.
Bei kleineren Zahlen kann man eine Primfaktorzerlegung oft direkt angeben. Bei größeren Zahlen sollte man systematisch vorgehen, um die Primfaktoren zu bestimmen. Bei noch größeren Zahlen ist es ratsam, die erforderlichen Berechnungen von einem Computer ausführen zu lassen - der kann das viel zuverlässiger und schneller.
Ein erster Algorithmus
b) Bei größeren Zahlen sollte man systematisch vorgehen, um die Primfaktoren zu bestimmen. Bestimme eine Primfaktorzerlegung von $n = 221$ und $n = 585$.
c) Entwickle einen Algorithmus zur Primfaktorzerlegung. Beschreibe das Verfahren als Pseudocode.
d) Entwickle ein Programm zur Primfaktordarstellung mit Processing. Es bietet sich an, eine Funktion int[] primfaktoren(int n)
zu erstellen, die die Liste der Primfaktoren zurückgibt.
Die folgende Übersicht verdeutlicht das Verfahren am Beispiel $n = 51$.
# Übergabe n = 51 # Initialisierung faktoren = [] {faktoren -> []} z = n {z -> 51}+ # Probedivisionen z % 2 -> 1 z % 3 -> 0 # Aktualisierung p = 3 {p -> 3} faktoren = faktoren + [p] {faktoren -> [3]} z = z // p {z -> 17} # Probedivisionen z % 2 -> 1 z % 3 -> 2 z % 4 -> 1 z % 5 -> 2 # Aktualisierung p = z {p -> 17} faktoren = faktoren + [p] {faktoren -> [3, 17]} z = z // p {z -> 1} # Rückgabe [3, 17]
Allgemein beschreiben lässt sich das Verfahren mit dem folgenden Algorithmus:
ALGORITHMUS primfaktoren(n): initialisiere die Liste faktoren: faktoren = [] initialisiere die Hilfsvariable z: z = n SOLANGE z > 1: bestimme den kleinsten Primfaktor p von z mit Probedivisionen füge p in die Liste faktoren ein z = z // p Rückgabe: faktoren
Die Processing-Implementierung sieht so aus:
- primfaktoren.pde
ArrayList<Long> primfaktoren(long n) { ArrayList<Long> faktoren = new ArrayList<Long>(); long z = n; while (z > 1) { long p = kleinsterPrimfaktor(z); faktoren.add(p); z = z / p; } return faktoren; } long kleinsterPrimfaktor(long n) { for (long p = 2; p <= sqrt(n); p++) { if (n % p == 0) return p; } return n; } void setup() { long[] numbers = new long[] { 11, 42, 48, 51, 100, 101, 221, 585, 1009, 10007 }; for (long number : numbers) { println(number, primfaktoren(number)); } } void draw() { }
primfaktoren(n)
. Wie zeigt sich, dass die Ausgangszahl eine Primzahl ist?
b) Bestimme mit der Funktion primfaktoren(n)
die Primfaktorzerlegung der beiden Zahlen $1000000000000000000L$ und $1000000000000000003L$ (das L
am Ende muss sein, damit Java die Zahl als long
erkennt!). Was stellst du fest? Stelle eine Vermutung auf, warum es hier zu einem unterschiedlichen Laufzeitverhalten kommt.
primfaktoren(n)
in diesem Fall genau die Faktorenliste [n]
.
b) $1000000000000000000$ ist schnell zu berechnen, $1000000000000000003$ braucht sehr lange, da es selbst eine Primzahl ist.
Messen der Laufzeit
Das folgende Testprogramm zeigt, wie man in Processing die Laufzeit messen kann.
// Erste Messung long time = System.nanoTime(); // Lange dauerndes Programm ausführen machWasDasSehrLangeDauert(); // Zweite Messung long delta = System.nanoTime() - time; double seconds = delta / 100000000d; println("Rechenzeit: ", seconds);
primfaktoren(n)
durch. Notiere dir die Laufzeit abhängig von der Eingabezahl.
Zunächst ist es günstig, sich auf Zahlen eines bestimmten Typs zu beschränken. Der Worst Case der Primfaktorzerlegung tritt auf, wenn viele Probedivisionen durchgeführt werden müssen, z.B. wenn die zu verarbeitende Zahl $n$ eine Primzahl ist. Dann muss $n$ durch alle Zahlen von $2$ bis $\sqrt{n}$ dividiert werden.
Interessiert man sich für den Zusammenhang zwischen der Größe der Zahl $n$ und der Laufzeit, sollte man $n$ systematisch vergrößern. Wir werden im Folgenden $n$ immer (in etwa) um eine 10er-Potenz (d.h. eine Dezimalstelle) vergrößern. Als Testzahlen werden der Reihe nach die jeweils kleinsten Primzahlen genommen, die größer als die nächste 10er-Potenz sind:
$$11,101,1009,10007,100003,1000003,10000019,100000007,1000000007,10000000019,100000000003,1000000000039,...$$
long
nur Zahlen bis 9223372036854775807 speichern kann, wurde für die Vorlage der Datentyp BigInteger
gewählt.
b) Führe die Laufzeitmessungen durch. Entscheide selbst, wann du die Ausführung der Berechnungen abbrichst. Warum solltest du während der Messreihe keine anderen Programme starten und verwenden?
- primfaktoren_zeit.pde
import java.math.BigInteger; BigInteger TWO = new BigInteger("2"); ArrayList<BigInteger> primfaktoren(BigInteger n) { ArrayList<BigInteger> faktoren = new ArrayList<BigInteger>(); BigInteger z = n; while (z.compareTo(BigInteger.ONE) > 0) { BigInteger p = kleinsterPrimfaktor(z); faktoren.add(p); z = z.divide(p); } return faktoren; } BigInteger kleinsterPrimfaktor(BigInteger n) { for (BigInteger p = TWO; p.compareTo(n.sqrt()) <= 0; p = p.add(BigInteger.ONE)) { if (n.mod(p).compareTo(BigInteger.ZERO) == 0) return p; } return n; } void setup() { String[] numbers = new String[] { "11", "101", "1009", "10007", "100003", "1000003", "10000019", "100000007", "1000000007", "10000000019", "100000000003", "1000000000039", "10000000000037", "100000000000031", "1000000000000037", "10000000000000061", "100000000000000003", "1000000000000000003", "10000000000000000051", "100000000000000000039", "1000000000000000000117", "10000000000000000000009", "100000000000000000000117", "1000000000000000000000007", "10000000000000000000000013", "100000000000000000000000067", "1000000000000000000000000103", "10000000000000000000000000331", "100000000000000000000000000319" }; for (String number : numbers) { long time = System.nanoTime(); ArrayList<BigInteger> factors = primfaktoren(new BigInteger(number)); long delta = System.nanoTime() - time; println(factors, "->", delta / 100_000_000d, "sec."); } } void draw() { }
Gesetzmäßigkeiten herausfinden
Folgende Messergebnisse erhält man, wenn man systematische Laufzeitmessungen durchführt. Beachte, dass die Messergebnisse vom benutzten Rechner abhängen. Deine Messergebnisse werden sich daher wohl von den hier gezeigten unterscheiden.
[11] -> 0.00043369 sec. [101] -> 0.00124726 sec. [1009] -> 0.00342708 sec. [10007] -> 0.01014742 sec. [100003] -> 0.02089646 sec. [1000003] -> 0.02341614 sec. [10000019] -> 0.02533699 sec. [100000007] -> 0.0784865 sec. [1000000007] -> 0.12939983 sec. [10000000019] -> 0.24801863 sec. [100000000003] -> 0.32550985 sec. [1000000000039] -> 0.92951605 sec. [10000000000037] -> 1.92971514 sec. [100000000000031] -> 5.73045039 sec. [1000000000000037] -> 17.88940701 sec. [10000000000000061] -> 56.39213684 sec. [100000000000000003] -> 175.94894516 sec. [1000000000000000003] -> 555.15387059 sec. [10000000000000000051] -> 5306.68616176 sec. ???
Es handelt sich hier um ein exponentielles Wachstumsverhalten, das man mathematisch mit einer Exponentialfunktion beschreiben kann: Wenn $k$ die Anzahl der Stellen der Ausgangszahl $n$ angibt, dann erhält man eine Laufzeit vom Typ $L(k) = c\cdot\sqrt{10}^k$ mit einer (zunächst noch unbekannten) Konstanten $c$.
Primzahl $n$ | Länge $k$ | Laufzeit [Sek.] |
---|---|---|
11 | 2 | 0.0000020000 |
101 | 3 | 0.0000020000 |
1009 | 4 | 0.0000030000 |
10007 | 5 | 0.0000070000 |
100003 | 6 | 0.0000210000 |
10…03 | 7 | 0.0000720000 |
10…019 | 8 | 0.0002380000 |
10…07 | 9 | 0.0007480000 |
10…07 | 10 | 0.0023330000 |
10…019 | 11 | 0.0089420000 |
10…03 | 12 | 0.0287030000 |
10…039 | 13 | 0.0925700000 |
10…037 | 14 | 0.2831490000 |
10…031 | 15 | 0.8942920000 |
10…037 | 16 | 2.8316820000 |
10…061 | 17 | 9.2346320000 |
10…03 | 18 | 29.4374940000 |
10…03 | 19 | 91.8103500000 |
10…051 | 20 | 393.7519600000 |
Überprüft man die theoretischen Überlegungen von vorhin mit der so berechneten Konstante $c=0.0000000426$, so ergibt sich die Funktion $0.0000000426\cdot \sqrt{10}^{k}$ (grauer Graph). Somit sind die theoretischen Überlegungen (zumindest für diesen Anwendungsfall und diesen Durchlauf) bestätigt.
Wenn man das Laufzeitverhalten eines Algorithmus / Programms mit einer Gesetzmäßigkeit beschreiben kann, dann lässt sich diese Gesetzmäßigkeit benutzen, um Prognosen zu erstellen.
Beim vorliegenden Programm zur Primfaktorzerlegung benötigt man etwa 1 Sekunde, um eine 15-stellige Zahl im worst case zu faktorisieren. Wenn die Zahl 100 Stellen haben soll, kann man die worst-case-Laufzeit mit der o.g. Funktionsgleichung berechnen, also $0.0000000426\cdot \sqrt{10}^{100} = 4.26 \cdot 10^{42} $ Sekunden, was etwa $1.351\cdot 10^{35}$ Jahren entspricht…
Aufwandsabschätzungen und Komplexitätsbetrachtungen
Lies Aufwandsabschätzungen und Komplexitätsbetrachtungen auf inf-schule.de. Beantworte danach die folgenden Fragen:
- Warum Aufwandsabschätzungen?
- Was versteht man unter "Problemgröße"?
- Was versteht man unter best/average/worst case?
- Was ist eine Kostenfunktion?
- Wann gelten Algorithmen als praktisch anwendbar?
- Warum Komplexitätsbetrachtungen?
- Was versteht man unter Zeitkomplexität?
- Wie könnte man den Probedivisionsalgorithmus verbessern?
- Tipp gefällig?Informiere dich über das Sieb des Eratosthenes!