Proseminar
Programming Pearls
Algorithm Design Techniques
von Andreas Lenerz

Algorithmus A4

Eine weitere Möglichkeit das Problem zu lösen besteht in der Nutzung eines Divide&Conquer Algorithmus.
Prinzip: Da wir das Problem nicht auf einmal lösen können, zerlegen wir unseren Array in 2 gleich große Teile. Nun gibt es 3 Möglichkeiten:

x[0] x[1] ... x[n/2-1] x[n/2] x[n/2+1] ... x[n-2] x[n-1]

Im Divide-Schritt wird dieser Array zerlegt in:

x[0] x[1] ... x[n/2-1]
x[n/2] x[n/2+1] ... x[n-2] x[n-1]

Dieses Unterteilen des Arrays kann nun für die Teilarrays weiter fortgeführt werden, bis die Möglichkeit besteht das Teilproblem zu lösen. Dann muß auf geeignete Weise versucht werden, die Ergebnisse geschickt wieder zu einem Ganzen zu kombinieren. Alles in allem führen diese Überlegungen zu folgendem Algorithmus:

Eingabe: Array rationaler Zahlen x der Länge n

Algorithmus A4:
int maxsum4(l,u)
if (l > u)
return 0
if (l == u)
return max( 0 , x[u] )
m = ( l + u ) / 2
lmax = 0
sum = 0
for ( i=m ; i>=l ; i-- )
sum += x[i]
lmax = max( lmax , sum )
rmax = 0
sum = 0
for (m,u]
sum += x[i]
rmax = max( rmax , sum )
return max(
rmax + lmax,
maxsum4(l,m),
maxsum4(m+1,r)
)

Erläuterung:
lmax

maximale Summe im linken Teilarray ( blau ). Gesucht wird hierbei der Index i, für den die Summe sum x[i..n/2-1] maximal wird. Der Wert dieser Summe ist lmax

lmax

Wie lmax nur im rechten Teilarray ( grün ). Hier wird sum x[n/2..i] bestimmt.

m

ist der mittlere Wert zwischen l und u also linker und rechter Grenze


Die beiden if-Schleifen oben im Algorithmus dienen dem Rekursionsabbruch. Aufgehört werden kann genau dann, wenn die linke Grenze >= der rechten ist, denn dann ist die Lösung klar bestimmbar, da nur noch höchstens ein Element im Teilarray vorhanden ist.
Die folgenden beiden Abschnitte zur Bestimmung von lmax bzw. rmax funktionieren auf folgende Weise: ( am Bsp von lmax )
Die maximale Summe ist nach der Einleitung immer >= 0. Daher wird lmax bereits am Anfang auf 0 gesetzt, damit dies gewährleistet werden kann. Wie oben in der Erläuterung beschrieben, wird dann durch sukzessives Aufsummieren der x[j] die Teilsumme sum x[i..n/2-1] bestimmt, wobei die Tatsache von A2 genutzt wird, daß nämlich

sum x[i..n/2-1] = sum x[i+1..n/2-1] + x[i]

ist. Mit Hilfe der max Funktion ist am Ende der Schleife max sum x[i..n/2-1] bestimmt.
Wenn dann anschließend auf ähnliche Weise rmax bestimmt wird, kommt man zur eigentlichen Auswertung.
Hier gilt nun, daß der gesuchte Wert gerade die Summation des linken und rechten Maximums ist ( rot, eben genau dann, wenn das Maximum im Divide Schritt geteilt wurde ) oder, wenn die maximale Summe doch komplett in einem der geteilten Arrays liegt, durch einen weiteren Programmaufruf für die halbierten Arrays ermittelt werden kann ( blau bzw. grün ).

Laufzeit:
Die Laufzeit dieses Divide&Conquer Algorithmus kann als Funktion in n aufgefasst werden.

T( n ) = { 1 , n <= 1
2 T ( n/2 ) + O( n ) , sonst

Anschaulich bedeutet dies:

x[0] ... x[n/4] ... x[n/2] ... x[3n/4] ... x[n-1]

Nach dem ersten Divide Schritt, wird der Array in 2 Teilarrays zerteilt:

x[0] ... x[n/4] ... x[n/2-1]
x[n/2] ... x[3n/4] ... x[n-1]

Nach dem zweiten Divide Schritt, wird jeder Teilarray wiederum in 2 zerteilt:

x[0] ... x[n/4-1]
x[n/4] ... x[n/2-1]
x[n/2] ... x[3n/4-1]
x[3n/4] ... x[n-1]

...

Betrachten wir nun, wie oft ein Element für das Ergebnis benutzt wird, so kommen wir zu folgendem Ergebnis:

Teilungsgrad in der for-Schleife betrachtete Elemente des Ausgangsarrays
n

Jede der beiden for-Schleifen läuft über n/2 Elemente und summiert diese auf, was zur Folge hat, daß insgesamt 2 * n/2 Elemente = n Elemente benutzt werden.

n/2

Über jeden Teilarray laufen 2 for-Schleifen, die jeweils n/4 Elemente betrachten, also 2 * 2 * n/4 Elemente = n Elemente. Auch in diesem Schritt wird jedes Element ein Mal benutzt, um die Lösung zu ermitteln

n/4

Über jeden Teilarray laufen 2 for-Schleifen, die jeweils n/8 Elemente betrachten, also 2 * 2 * 2 * n/8 Elemente = n Elemente. Auch in diesem Schritt wird jedes Element ein Mal benutzt, um die Lösung zu ermitteln

...

Wie man schnell sieht, wird in jedem Schritt jedes Element genau 1 Mal benutzt. Die Länge des Arrays ist n, wir haben also n Elemente. Bleibt die Tiefe des Baumes zu bestimmen, damit gezeigt werden kann, wie oft n Elemente benutzt werden.
In jedem Schritt teilen sich in die Arrays in 2 Teilarrays gleicher Größe auf. Das Verfahren stoppt, sobald n<=1 errreicht ist. Somit ist die Frage, wann 0 < (0,5)^i * n <= 1 gilt, zu beantworten. Dies gilt aber gerade für i = log( n ), da
2^log( n ) = n , also da 2^-1 = 0,5
(0,5)^log ( n ) = 1/n , somit also

(0,5)^log( n ) * n = 1

Somit ist die Laufzeit des Verfahrens O( n * log( n ) )


Vorteile zu A1/A2/A3 Nachteile zu A1/A2/A3
  • wesentlich bessere Laufzeit für große n
  • komplizierter und unübersichtlicher durch Komplexität des Verfahrens
  • extrem hoher Speicherverbrauch durch Rekursion ( Rekursionsstack ... )

Im Test ergab sich das folgenden Ergebnis:
Für 10.000 Werte benötigte A2 eine Zeit von: 0 sec 10 msec
Für 100.000 Werte benötigte A2 eine Zeit von: 0 sec 90 msec

--> Weiter zu den Beispielen
--> Weiter zu Algorithmus A5
--> Zurück zum Hauptmenü