Projecteuler: Lösungen mit Python und C++

Die “Lösungen in Projecteuler”-Serie geht mit dem zweiten Teil und dem zweiten Problem weiter. Heute stelle ich wieder kurz das Problem vor und einige kleine Statistiken aus Projecteuler.net-Seite und dann gibt es zunächst eine straight-forward Lösung und zwei alternative Ansätze, die eventuell etwas schneller oder eleganter sind.

Problemvorstellung

Das zweite Problem von Projecteuler.net dreht sich um eine Zahlenfolge, die bei allen Mathematik- und Informatiklehrern beliebt zu sein scheint. Es handelt sich dabei um die Fibonacci-Folge, eine unendliche Folge von Zahlen, die sich jeweils aus den zwei vorhergehenden Zahlen durch Addition ergeben.

Im folgenden Beispiel beginnt die Fibonacci-Folge mit zwei Einsen:
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233 ...

Ein Herr Fibonacci hat der Legende nach versucht, eine ideal wachsende Kaninchenpopulation damit zu beschreiben. Er ist von zwei Kaninchen ausgegangen und ist auf diese Zahlenfolge mit folgenden Regeln gekommen:

  • das Spiel beginnt mit einem Kaninchenpärchen
  • pro Zyklus wirft jedes geschlechtsreife Pärchen ein weiteres Pärchen
  • jedes Pärchen wird im zweiten Zyklus geschlechtsreif
  • kein Tier stirbt, kein Tier kommt zusätzlich hinzu

Wendet man die Regeln an, so lebt im ersten Zyklus ein Pärchen. Im zweiten Zyklus leben schon 2 Pärchen, aber nur eines ist geschlechtsreif und wirft im dritten Zyklus ein weiteres Pärchen. Im vierten Zyklus werden zwei weitere Pärchen geworfen, weil es bereits zwei geschlechtsreife Pärchen gibt, das erste und das zweite Pärchen, welches inzwischen im zweiten Zyklus lebt. Auf diese Weise ergibt sich anschaulich die Fibonacci-Folge. Vielleicht lieben es die Lehrer deswegen so sehr :-) .

Etwas mathematischer ausgedrückt sieht es so aus:

a_{1}=1 \newline a_{2}=2 \newline  a_{i} = a_{i-2}+a_{i-1}

Damit haben wir das zweite Problem von Projecteuler.net etwas eingegrenzt und das Verständnis für die Fibonacci-Folge bildlich untermauert.

Doch worum geht es in dem Problem?

In der zweiten Aufgabe sollen wir alle Zahlen der Fibonacci-Folge, die kleiner als vier Millionen und gleichzeitig gerade sind, addieren.

Das war es schon.

Die Aufgabe gehört genauso, wie die erste zu den einfacheren Problemen. Zum heutigen Zeitpunkt haben 73397 der derzeit 95331 registrierten Hobbymathematiker diese Aufgabe gelöst, das sind immerhin stolze 77%.

Problemeingrenzung

Um der Herausforderung dieses Problems etwas auf die Schliche zu kommen, habe ich mir einfach alle Zahlen der Fibonacci-Folge ausgeben lassen, die kleiner als 4000000 sind. Dazu habe ich folgende kleine Funktion geschrieben:

def print_fibos(N):
    i = 1;

    fibs = np.zeros(1)
    fibs[0] = 1
    fibs = np.append(fibs,[2.],axis=0)
    while (fibs[i]+fibs[i-1])

Die Ausgabe zeigt genau 32 Zahlen. Würde man alle Zahlen addieren wollen, dann wären das 31 Additionen von Ganzzahlen, was nicht wirklich aufwendig ist. Deswegen denke ich, dass es eher sinnvoll ist, die Lösung dahingehend zu optimieren, dass möglichst wenige Tests durchgeführt werden müssen, ob eine Zahl gerade ist oder nicht.

Problemlösung

Wie bereits angekündigt stelle ich zunächst eine straight-forward Lösung vor. Hierbei geht Python alle Fibonacci-Zahlen durch und überprüft, ob sie kleiner als die Grenze von 4000000 ist und ob sie gerade ist. Sind beide Bedingungen erfüllt, so wird die Zahl zu einem Zähler addiert.
So sieht das in Python-Code aus:

import time as t
import numpy as np

def straight_forward(N):
    count  = 2;
    first  = 2;
    second = 3;
    next   = 0;

    while next < N:
        next = first+second;
        first = second;
        second = next;

        if next%2 == 0:
            count = count + next

    return count;

# max. fibo
N = 4000000

print_fibos(N);

t0 = t.time();
erg = straight_forward(N);
tn = t.time();

print("The straight-forward method needed %.15f seconds.\nThe sum is: %12i" %(tn-t0,erg));

Die Funktion gibt auf Anhieb die richtige Lösung von 4613732 aus.

Lösungsanalyse

In dem Code der straight-forward Lösung habe ich bereits die Zeitmessung mit eingebaut. Die Ausgabe des kleinen Programms liefert:

The straigt-forward method needed 0.000010013580322 seconds.
The sum is: 4613732

Ehrlich gesagt, ist die Ausgabe der Zeit in diesem Fall wohl eher nicht sehr zuverlässig. Mir geht es jedoch mehr um die Tendenz, weniger um den absoluten Wert, daher ist es ok.

Also hat die straight-forward Lösung in einer Zeit von 10 Mikrosekunden 10 Zahlen addiert und 31 Zahlen auf die Teilbarkeit durch zwei geprüft. Da die Aufgabe noch recht leicht ist, muss es ja auch besser gehen. Darum geht es im nächsten Abschnitt.

alternative Lösung

Die Problemeingrenzung hat uns gezeigt, dass es gilt, die Teilbarkeitstest durch zwei so selten wie nur möglich durchzuführen sind. Am Besten wären gar keine Tests.

Schauen wir uns die Folge der geraden Fibonacci-Zahlen näher an:
1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233 ...
Es wird deutlich, dass ab der “2″ jede dritte Zahl eine gerade Zahl ist. Folglich müssen auch nur diese summiert werden. Aus diesen Überlegungen hat sich dann folgender Code ergeben:

import time as t
import numpy as np

def only_even(N):
    count = 2;
    a = 1;
    b = 2;
    next = 0;

    while count < N:
        next = 2*a+3*b
        a = 2*b+a
        b = next
        count = count + next;
    return count;

N=4000000;

t0 = t.time();
erg = only_even(N);
tn = t.time();

print("The only_even method needed %.15f seconds.\nThe sum is: %12i" %(tn-t0,erg));

Die Anweisung zur Berechnung der nächsten gerade Fibonacci-Zahl ergibt sich aus folgender Betrachtung:

next = a + b; \: a^{'}=b;\: b^{'}=next \newline next^{'} = a^{'} + b^{'}; \: a^{''}=b^{'};\: b^{''}=next^{''} \newline next^{''} = a^{''} + b^{''}; \: a^{'''}=b^{''};\: b^{'''}=next^{'''} \newline

Setzt man nun von unten nach oben die jeweiligen Variablen ein, so ergibt sich die oben aufgeführte Formel.

Doch welche Vorteile bringt diese Lösung? Folgende Ausgabe erhält man beim Ausführen:
The only_even method needed 0.000007867813110 seconds.
The sum is: 4613732
Also haben wir damit etwas weniger Wartezeit und sind somit effizienter.

Ok, das ist schon besser, aber ich war noch nicht zufrieden und habe noch eine Lösung ausgetestet:

alternative Lösung 2

Mit der zweite alternative Lösung habe ich einen rekursiven Ansatz verfolgt:

import time as t
import numpy as np
def rec_fibo(a,b,N):
    next = 2*a + 3*b;
    if (next < N):
        return next + rec_fibo(2*b+a,next,N);
    else:
        return 0;

t0 = t.time();
erg = rec_fibo(1,2,N);
tn = t.time();

print("The recursive method needed %.15f seconds.\nThe sum is: %12i" %(tn-t0,erg));

Wie du bestimmt festgestellt hast, verwendet auch die rekursive Lösung keine Gradheitsüberprüfung der Fibonacci-Zahlen. Hier habe ich genauso, wie bei der ersten alternativen Lösung nur die geraden Zahlen betrachtet.

Die Rekursion bietet sich oft für Probleme wie dieses an. Es ergibt meist eine recht elegante und kurze Lösung, die aber nicht zwangsläufig auch weniger Rechenzeit benötigt, wie die Ausgabe zeigt:
The straigt-forward method needed 0.000010013580322 seconds.
The sum is: 4613732
The only_even method needed 0.000007867813110 seconds.
The sum is: 4613732
The recursive method needed 0.000009059906006 seconds.
The sum is: 4613730

Zusammengefasst ist die einfache Lösung ohne Gradheitsüberprüfung die schnellste, jedoch sind die Zeiten bei dieser Dimension des Problems mit Vorsicht zu genießen, sie sind ziemlich klein.

Fazit

Damit wäre ich am Ende meiner Lösungsvorstellung des zweiten Projecteuler.net Problems. Diesmal gab es wieder eine Neuerung, die bisher in der Form in meinem Weblog noch nicht vorkam: die Rekursion.

Jetzt ist wieder deine Meinung gefragt: Was hat dir gefallen, was nicht und was würdest du mir als Tipp für die nächste Folge "Lösungen mit Python und C++" mitgeben?

Scribtee - Designer T-Shirts

Artikel aus der selben Kategorie:

Es gibt noch keine Kommentare zu “Projecteuler: Lösung zu Problem 2”.
Jede Meinung ist willkommen!

Meinungsfreiheit für alle!