====== Summierung von Aktienpositionen ====== ===== Problemstellung ===== In unserem Beispiel verwalten wir eine größere Menge an Aktienpositionen. Diese Positionen haben jeweils einen Wert zugewiesen. Nun wollen wir für eine aufeinanderfolgende Gruppe an Aktienpositionen den Gesamtwert abfragen. ===== Eingabe ===== Als Eingabe bekommen wir die Anzahl an Aktien, deren Werte, die Anzahl an Abfragen sowie die Abfragen selbst. Sehen wir uns ein Beispiel an: ^ Variable ^ Wert ^ | Anzahl an Aktienpositionen | 5 | | Anzahl an Abfragen | 3 | Danach folgen die entsprechenden Werte: ^ Index ^ Wert ^ | 0 | 8 | | 1 | 13 | | 2 | 4 | | 3 | 6 | | 4 | 10 | Und schlussendlich die Abfragen: ^ Index ^ Start-Index ^ End-Index ^ | 0 | 1 | 2 | | 1 | 0 | 4 | | 2 | 4 | 4 | In unseren Test-Programmen werden die Daten über die Standardeingabe eingelesen. Bzw. in ausgeschriebener Form: 5 8 13 4 6 10 3 1 2 0 4 4 4 ===== Quadratische Lösung ===== Eine Möglichkeit eine Abfrage zu beantworten ist, jedes Mal über die entsprechenden Aktienwerte zu iterieren und diese zu addieren. #include #include int main() { // Anzahl an Aktien einlesen int num_stocks; scanf("%d", &num_stocks); if (num_stocks < 1) { printf("Anzahl der Aktienpositionen muss mindestens 1 sein.\n"); return 1; } // Werte einlesen int *prices = (int *)malloc(num_stocks * sizeof(int)); for (int i = 0; i < num_stocks; i++) scanf("%d", &(prices[i])); // Anzahl an Abfragen einlesen int num_queries; scanf("%d", &num_queries); if (num_queries < 1) { printf("Anzahl der Abfragen muss mindestens 1 sein.\n"); return 1; } // Abfragen einlesen int *queries = (int *)malloc(num_queries * 2 * sizeof(int)); for (int i = 0; i < num_queries; i++) { scanf("%d %d", &(queries[i * 2]), &(queries[i * 2 + 1])); if (queries[i * 2] > queries[i * 2 + 1]) { printf("Ungültige Abfrage\n"); return 1; } } // Alle Abfragen berechnen long total_sum = 0; for (int i = 0; i < num_queries; i++) { long query_sum = 0; for (int price_index = queries[i * 2]; price_index <= queries[i * 2 + 1]; price_index++) query_sum += prices[price_index]; total_sum += query_sum; } // Ergebnis ausgeben printf("Gesamtsumme: %ld\n", total_sum); free(queries); free(prices); return 0; } Aufruf: $ ./squared_stock_prices < stocks_example_input.txt Ausgabe: Gesamtsumme: 68 Sehen wir uns nun die Anzahl der für die Berechnung notwendigen Schritte an. Folgende beiden Schleifen sind für die Berechnung der Summe verantwortlich: for (int i = 0; i < num_queries; i++) { long query_sum = 0; for (int price_index = queries[i * 2]; price_index <= queries[i * 2 + 1]; price_index++) query_sum += prices[price_index]; total_sum += query_sum; } Die äußere Schleife startet bei 0 und läuft gegen ''num_queries'', hat also genau ''num_queries'' Iterationen. In der inneren Schleife iterieren wir über den abgefragten Bereich. Der aufwendigste Fall mit den meisten Schritten ist also jener, bei dem wir alle vorhandenen Werte aufsummieren. In diesem Fall hat die innere Schleife ''num_stocks'' Schritte. Nun haben wir für jeden der ''num_queries'' Durchläufe der äußeren Schleife im schlimmsten Fall ''num_stocks'' Durchläufe in der inneren Schleife. In Kurzschreibweise bedeutet das: **''O(num_stocks * num_queries)''** Gehen wir davon aus, dass sich ''num_stocks'' und ''num_queries'' in der gleichen Größenordnung befinden, können wir diese Implementierung überschlagsmäßig als quadratisch bezeichnen. ===== Lineare Lösung ===== Die obenstehende Lösung funktioniert in unserem Beispiel ganz gut. Aber was ist, wenn wir nicht fünf sondern ein paar Millionen Kurspositionen haben? Oder auch eine wesentlich größere Anzahl an Abfragen? Versuchen wir eine bessere Lösung mit der Präfixsumme zu finden. Für jede Abfrage werden alle Werte im angegebenen Bereich aufsummiert. Selbst wenn die selbe Abfrage doppelt auftritt, wiederholen wir die Berechnung. Betrachten wir nun Bereichsabfragen als Teilproblem der vollständigen Summe aller Werte. Um das Ergebnis einer Bereichsabfrage zu erhalten, müssen die Summanden vor und nach dem Bereich weggelassen werden. Folgende Tabelle enthält neben der Eingabe auch die dazugehörige Präfixsumme. ^ Index ^ Wert ^ Präfixsumme ^ | 0 | 8 | 8 | | 1 | 13 | 21 | | 2 | 4 | 25 | | 3 | 6 | 31 | | 4 | 10 | 41 | Als Beispiel nehmen wir die Abfrage der Indizes von 1 bis inklusive 2 an. Die Präfixsumme hat beim End-Index laut Tabelle den Wert 25. Um nachfolgende Elemente müssen wir uns an dieser Stelle nicht kümmern, diese sind in diesem Wert nicht enthalten. Vorhergehend Werte können wir über die Präfixsumme beim Index direkt vor dem abgefragten Teilbereich abziehen, in unserem Fall also bei Index 0. Wir berechnen also ''25 - 8 = 17'', die manuelle Überprüfung ''13 + 4 = 17'' bestätigt das Ergebnis. Mit dieser Optimierung können wir die Summen für Abfragen in konstanter Zeit nutzen. #include #include int main() { // Anzahl an Aktien einlesen int num_stocks; scanf("%d", &num_stocks); if (num_stocks < 1) { printf("Anzahl der Aktienpositionen muss mindestens 1 sein.\n"); return 1; } // Werte einlesen long *price_sums = (long *)malloc((num_stocks + 1) * sizeof(long)); // Erstes Element überspringen, damit wir beim Aufsummieren darauf // zugreifen können price_sums[0] = 0; price_sums++; for (int i = 0; i < num_stocks; i++) { scanf("%ld", &(price_sums[i])); price_sums[i] += price_sums[i - 1]; } // Anzahl an Abfragen einlesen int num_queries; scanf("%d", &num_queries); if (num_queries < 1) { printf("Anzahl der Abfragen muss mindestens 1 sein.\n"); return 1; } // Abfragen einlesen int *queries = (int *)malloc(num_queries * 2 * sizeof(int)); for (int i = 0; i < num_queries; i++) { scanf("%d %d", &(queries[i * 2]), &(queries[i * 2 + 1])); if (queries[i * 2] > queries[i * 2 + 1]) { printf("Ungueltige Abfrage\n"); return 1; } } // Alle Abfragen berechnen long total_sum = 0; for (int i = 0; i < num_queries; i++) { long query_sum = price_sums[queries[i * 2 + 1]] - price_sums[queries[i * 2] - 1]; total_sum += query_sum; } // Ergebnis ausgeben printf("Gesamtsumme: %ld\n", total_sum); free(queries); free(price_sums - 1); return 0; } Aufruf: $ ./linear_stock_prices < stocks_example_input.txt Ausgabe: Gesamtsumme: 68 Die initiale Berechnung der Präfixsumme kostet uns ''num_stocks'' Schritte. Danach berechnen wir ''num_queries'' Abfragen in konstanter Zeit. In dieser Version haben wir den rechnerischen Aufwand also von ''O(num_stocks * num_queries)'' auf **''O(num_stocks + num_queries)''** verringert. ===== Performance-Vergleich ===== Nachfolgend wollen wir einen einfachen Performance-Vergleich zwischen den beiden Implementierungen durchführen. Es ist zu beachten, dass dies kein vollständiger und statistisch zuverlässiger Benchmark ist. Vielmehr soll der Sinn hinter dieser Art von Optimierung demonstriert werden. Für einen sinnvollen Vergleich sind große Datenmengen erforderlich. Um die entsprechenden Eingabedaten zu generieren, wird ein separates Programm verwendet: #include #include #include #include int main(int argc, char *argv[]) { if (argc != 4) { printf("Usage: %s num_stocks num_queries max_value\n", argv[0]); return 1; } // Der Zufallsgenerator wird mit der aktuellen Zeit initialisiert. srand(time(NULL)); // Anzahl der zu generierenden Werte und Abfragen werden als // Kommandozeilenparameter übergeben. const int num_stocks = atoi(argv[1]), num_queries = atoi(argv[2]), max_value = atoi(argv[3]); // Anzahl der Werte sowie Werte selbst ausgeben printf("%d\n", num_stocks); // In der Schleife folgt die zufällige Generirung // und Ausgabe der Werte. for (int i = 0; i < num_stocks; i++) printf("%d\n", rand() % (max_value + 1)); // Anzahl der Abfragen sowie Abfragen selbst ausgeben printf("%d\n", num_queries); // In der Schleife folgt die zufällige Generirung // und Ausgabe der Abfragen. for (int i = 0; i < num_queries; i++) { const int query_start = rand() % num_stocks; const int query_end = query_start + (rand() % (num_stocks - query_start)); printf("%d %d\n", query_start, query_end); } return 0; } Nun erzeugen wir uns in 500.000er Schritten sechs Testfälle. Aufruf: $ for s in `seq 500000 500000 3000000`; do ./stocks_cumulative_sum_generator "$s" "$s" 10 > "stocks_${s}x${s}.txt"; done Beide Implementierungen werden auf höchster Optimierungsstufe kompiliert: $ gcc -O3 squared_stock_prices.c -o squared_stock_prices $ gcc -O3 linear_stock_prices.c -o linear_stock_prices ^ Datei ^ Dauer quadratische Ausführung ^ Dauer lineare Ausführung ^ | stocks_500000x500000.txt | 18,666 | 0,114 | | stocks_1000000x1000000.txt | 1:16,45 | 0,213 | | stocks_1500000x1500000.txt | 3:06,29 | 0,333 | | stocks_2000000x2000000.txt | 6:25,36 | 0,436 | | stocks_2500000x2500000.txt | 11:10,95 | 0,552 | | stocks_3000000x3000000.txt | 16:25,66 | 0,675 | Die optimierte Lösung benötigt für das gleiche Ergebnis deutlich weniger Zeit. Natürlich werden bei diesem Vergleich einige wesentliche Dinge vernachlässigt, wie etwa die verwendete Hardware oder Caches. Das Ergebnis ist trotzdem relativ eindeutig. ===== Siehe auch ===== * [[algo:prefix-sum:start]] * [[algo:prefix-sum:optimization:area]]