Przeskocz do treści

Delta mi!

Informatyk gra na giełdzie

Tomasz Idziaszek

o artykule ...

  • Publikacja w Delcie: październik 2013
  • Publikacja elektroniczna: 01-10-2013
  • Wersja do druku [application/pdf]: (428 KB)

Nasz znajomy informatyk zdecydował się zainwestować część swoich oszczędności na giełdzie papierów wartościowych. Jak na informatyka przystało, do grania na giełdzie postanowił zaprząc komputer. W tym celu, korzystając z najnowszych trendów sztucznej inteligencji, napisał program, który na podstawie przeszłych notowań giełdowych przewiduje, jak kurs akcji będzie się zmieniał w przyszłości, i podejmuje decyzje o kupnie bądź sprzedaży. Nasz znajomy przetestował program, uruchamiając go na dużym zbiorze archiwalnych notowań. Zastanawia się teraz, jak dobrze jego program sobie poradził – stanął zatem przed problemem wyznaczenia najlepszej możliwej gry na giełdzie, jeśli znamy wszystkie notowania.

obrazek

Model gry na giełdzie będzie następujący. Mamy daną tablicę math  z notowaniami giełdowymi w kolejnych dniach: math  oznacza cenę jednej akcji w  math-tym dniu (dla uproszczenia przyjmiemy, że mamy tylko jeden rodzaj akcji). Na początku dysponujemy kwotą math i możemy wykonać nie więcej niż math  operacji kupna-sprzedaży akcji (zakładamy, że możemy kupować ułamkową liczbę akcji). Zauważmy, że nie potrzebujemy wykonywać operacji równolegle (tzn. przed każdym kupnem opłaca się nam najpierw sprzedać wszystkie posiadane akcje). Ponadto warto też kupować akcje za całą dostępną kwotę. Z tego wynika, że jeśli pierwszą operację kupna przeprowadzimy w dniu math a odpowiadającą jej sprzedaż w dniu math to po tej operacji będziemy mieli kwotę math  Naszym celem jest zmaksymalizowanie kwoty po wszystkich math  operacjach, czyli iloczynu

display-math

Możemy pozbyć się mnożeń i dzieleń, logarytmując powyższy wzór. Innymi słowy, równoważnie możemy zmaksymalizować sumę

display-math

obrazek

Rys. 1 Przykładowa tablica math dla math Optymalne rozwiązanie dla math to dwa zaznaczone fragmenty o sumach 7 i 6.

Rys. 1 Przykładowa tablica math dla math Optymalne rozwiązanie dla math to dwa zaznaczone fragmenty o sumach 7 i 6.

obrazek

W końcu jeśli wprowadzimy pomocniczą tablicę math która zawierać będzie zmiany zlogarytmowanych notowań, tzn. math  to nasze zadanie sprowadzi się do wybrania co najwyżej math  rozłącznych fragmentów math które maksymalizują sumę liczb do nich należących (Rys. 1):

display-math

Dla math  jest to klasyczne zadanie znajdowania fragmentu tablicy o największej sumie, które może być znane Czytelnikom. Z kolei jego uogólnienie dla math  było treścią zadania pt. Tanie linie, które pojawiło się podczas weekendowej rundy Potyczek Algorytmicznych 2012. Oba problemy mają liczne ciekawe rozwiązania. W dalszej części artykułu przedstawimy, z pożytkiem dla znajomego informatyka, aż osiem z nich.

***

Na początek przypomnijmy, jak rozwiązać zadanie dla math  Algorytm A1 jest bardzo prosty: w czasie math  możemy przebadać wszystkie możliwe fragmenty, ustalając lewy koniec fragmentu i iterując po kolejnych możliwych prawych końcach.

Algorytm A2 wykona to samo w optymalnym czasie math  Przeglądamy elementy tablicy od lewej do prawej i trzymamy math – maksymalny fragment w przedziale math oraz math – maksymalny fragment, który dotyka prawego końca tego przedziału (tzn. kończy się elementem math). Na początek math i  math inicjujemy zerami, a następnie wykonujemy pętlę:

display-math

Zadanie dla math  można rozwiązać, korzystając z metody programowania dynamicznego i uogólniając algorytm A2. Przez math oznaczmy największą sumę liczb z przedziału math zawartych w co najwyżej math rozłącznych fragmentach, a przez math to samo, ale z zastrzeżeniem, że ostatni fragment zawiera element math Rekurencja (bez uwzględniania warunków brzegowych) jest następująca:

pict

Rozwiązaniem jest math ; algorytm A3 działa w czasie math

obrazek
obrazek

Rys. 2 Rozwiązania optymalne zawierające fragment math oraz fragmenty math i math (szare obszary), jak również nowe rozwiązanie optymalne dla dwóch fragmentów (przerywana linia).

Rys. 2 Rozwiązania optymalne zawierające fragment math oraz fragmenty math i math (szare obszary), jak również nowe rozwiązanie optymalne dla dwóch fragmentów (przerywana linia).

Poszukując szybszego algorytmu dla math  spróbujmy odpowiedzieć na pytanie: Czy, mając optymalne rozwiązanie dla math  fragmentów, da się je łatwo rozszerzyć do math  fragmentów? Zatrzymajmy się nad przypadkiem math  Powiedzmy, że fragment math ma największą sumę. Jak może wyglądać optymalne rozwiązanie dla dwóch fragmentów math Rozważmy dwa przypadki (Rys. 2).

  • (1) Jeden z fragmentów (powiedzmy math) jest rozłączny z  math Wtedy math jest poprawnym rozwiązaniem, zatem z optymalności math dostajemy, że math Ale z optymalności math mamy math zatem fragmenty math i  math mają tę samą sumę. To pokazuje, że math jest również optymalne.
  • (2) Oba fragmenty math przecinają math Jeśli math to fragment math musi mieć sumę zero (inaczej moglibyśmy poprawić math dodając do niego ten fragment, lub poprawić math usuwając ten fragment). Zatem usunięcie tego fragmentu z  math nie zmieni wyniku. Analogicznie dla math fragment math musi mieć sumę zero i można go dodać do math Stosując takie samo rozumowanie do prawego końca fragmentu math dostajemy, że istnieje optymalne rozwiązanie math w którym math i  math

Udowodniliśmy zatem, że możemy znaleźć optymalne rozwiązanie dla math  rozszerzając fragment math o największej sumie. W tym celu: albo (1) dodajemy nowy fragment o największej sumie, który jest rozłączny z  math albo (2) znajdujemy fragment o najmniejszej sumie zawarty całkowicie w  math i usuwamy go, dzieląc math na dwa fragmenty. Wybieramy ten wariant, który lepiej poprawia wynik.

obrazek

Rys. 3 Podział tablicy po wykonaniu dwóch faz algorytmu.

Rys. 3 Podział tablicy po wykonaniu dwóch faz algorytmu.

Zachęceni tym sukcesem moglibyśmy wykonać trochę eksperymentów praktycznych i przekonać się, że pomysł ten działa dla dowolnego math  Niech math  to zbiór fragmentów o maksymalnej łącznej sumie, a  math  to pozostałe części tablicy (Rys. 3). Aby uzyskać optymalne math  fragmentów, albo dołączamy maksymalny fragment, który jest zawarty w pewnym math  albo usuwamy minimalny fragment z pewnego math Zachęcamy do próby dowodu poprawności tego rozwiązania. To może nie być proste, ale dla Czytelników Wytrwałych na końcu artykułu podamy wskazówkę, jak można się do tego zabrać.

Pozostaje kwestia efektywnej implementacji tego pomysłu. Jedną fazę możemy wykonać w czasie math  uruchamiając algorytm A2 na każdym przedziale osobno. Tak zapisany algorytm A4 będzie działał w czasie math  co nie daje nam jeszcze zysku w porównaniu z algorytmem A3. Widać, że kluczową operacją jest odpowiadanie na pytania „jaki jest maksymalny fragment w danym przedziale?” dla różnych przedziałów. Pokażemy teraz, jak to robić efektywnie.

obrazek

Rys. 4 Możliwe pozycje maksymalnego fragmentu math w zależności od fragmentów w lewej i prawej połowie przedziału.

Rys. 4 Możliwe pozycje maksymalnego fragmentu math w zależności od fragmentów w lewej i prawej połowie przedziału.

obrazek

Rys. 5 Drzewo przedziałowe dla math ma math węzłów. Węzeł 5 odpowiada przedziałowi bazowemu math Przedział math można podzielić na przedziały bazowe, którym odpowiadają węzły 9, 5 i 6.

Rys. 5 Drzewo przedziałowe dla math ma math węzłów. Węzeł 5 odpowiada przedziałowi bazowemu math Przedział math można podzielić na przedziały bazowe, którym odpowiadają węzły 9, 5 i 6.

W tym celu może nam pomóc jeszcze jeden algorytm dla math  Algorytm A5 będzie oparty o metodę „dziel i zwyciężaj”. Mając dany przedział o długości math możemy go podzielić na dwie części o długości math Maksymalny fragment w tym przedziale może znajdować się w całości w lewej części, w całości w prawej części lub może składać się z maksymalnego fragmentu, który dotyka prawej krawędzi lewej części, oraz maksymalnego fragmentu, który dotyka lewej krawędzi prawej części (por. też Rys. 4).

Załóżmy, że math  i zbudujmy drzewo przedziałowe (Rys. 5). Drzewo będzie miało węzły o numerach od 1 do math  W węźle math dla math tego drzewa będą znajdować się informacje o przedziale math  a konkretnie: math – suma liczb w przedziale, math – maksymalny fragment w przedziale oraz math i  math – maksymalne fragmenty dotykające odpowiednio lewego i prawego końca przedziału. Wartości w węzłach najniższego poziomu (tzn. dla math ) inicjujemy, przyjmując math  oraz math Wartości w pozostałych węzłach math wyznaczamy na podstawie wartości w węzłach math i  math które odpowiadają lewej i prawej połowie przedziału:

pict

Wyznaczenie wartości we wszystkich węzłach zabiera czas math  i w takim czasie działa algorytm A5. Odpowiedzią jest, oczywiście, wartość math

Skonstruowane drzewo przedziałowe wyróżnia się tym, że umożliwia ono znalezienie największego fragmentu dla dowolnego przedziału tablicy w czasie math  W tym celu przypomnijmy, że każdy przedział można podzielić na math  przedziałów bazowych, tzn. przedziałów, które odpowiadają węzłom drzewa przedziałowego (Rys. 5). Niech math będą kolejnymi węzłami odpowiadającymi takiemu podziałowi. Wtedy maksymalny fragment to będzie albo math dla pewnego math albo

display-math

dla pewnych math 

Poniższa pętla wyznacza maksymalny fragment math  w czasie math :

display-math

Algorytm A6 jest następujący: najpierw budujemy drzewo przedziałowe (jak w algorytmie A5) oraz drugie drzewo przedziałowe, które będzie liczyło minimalne fragmenty. Oprócz wartości math będziemy potrzebować również końców fragmentów – odpowiednie wzbogacenie drzewa przedziałowego zostawiamy jako ćwiczenie dla Czytelników. Wszystkie przedziały trzymamy w kolejce priorytetowej: przedziały math z priorytetami równymi minimalnym fragmentom w tych przedziałach, zaś przedziały math  z priorytetami równymi maksymalnym fragmentom.

Każdy krok algorytmu to wyciągnięcie z kolejki przedziału o priorytecie o największej wartości bezwzględnej, uaktualnienie wyniku o wartość bezwzględną tego priorytetu, a następnie dodanie trzech nowych przedziałów do kolejki. Cały algorytm działa w czasie math

obrazek

***

Algorytm A6 jest efektywny, ale dość skomplikowany w implementacji. Przedstawimy teraz prostszy (choć nieco zaskakujący) algorytm, na który autor artykułu wpadł, próbując udowodnić poprawność algorytmów A4A6. Algorytm ten korzysta z metody „spróbujmy to zrobić od końca”.

obrazek

Rys. 6 Ciąg po podziale na bloki, powstały z tablicy liczb z rysunku 1, oraz ten sam ciąg po pierwszej fazie skracania.

Rys. 6 Ciąg po podziale na bloki, powstały z tablicy liczb z rysunku 1, oraz ten sam ciąg po pierwszej fazie skracania.

Wypiszmy liczby z tablicy w ciągu i podzielmy go na maksymalne bloki liczb o tym samym znaku (na potrzeby definicji bloku traktujemy 0 jako liczbę dodatnią). Zauważmy, że w optymalnym rozwiązaniu każdy fragment musi zaczynać się i kończyć pełnym blokiem, który zawiera liczby dodatnie (jeśli kończyłby się niepełnym blokiem dodatnim, to moglibyśmy ten fragment rozszerzyć, uzyskując nie gorsze rozwiązanie, a gdyby kończył się blokiem ujemnym – moglibyśmy go skrócić). Możemy więc zastąpić każdy blok przez jedną liczbę będącą sumą elementów tego bloku. Dodatkowo dodajmy na obu końcach ciągu liczbę-strażnika math (Rys. 6).

Jeśli w nowym ciągu mamy co najwyżej math  liczb dodatnich, to ich suma jest rozwiązaniem zadania. W przeciwnym przypadku będziemy iteracyjnie skracać ciąg, nie zmieniając optymalnego rozwiązania.

Faza skracania jest następująca: wybieramy liczbę w ciągu o najmniejszej wartości bezwzględnej, math a następnie zastępujemy ją i dwie liczby z nią sąsiadujące – ich sumą. Zauważmy, że nowa liczba math będzie miała przeciwny znak do math więc krótszy ciąg nadal będzie zawierał naprzemiennie liczby dodatnie i ujemne. Fazę skracania można wykonać w czasie math – wystarczy trzymać elementy ciągu na liście, a poza tym mieć kolejkę priorytetową z liczbami uporządkowanymi względem ich wartości bezwzględnych. Zatem algorytm A8 będzie działał w czasie math

obrazek

Rys. 7 Konstrukcja optymalnego rozwiązania niezawierającego wyróżnionych trzech liczb (linia przerywana) na podstawie dowolnego rozwiązania optymalnego niezawierającego którejś z tych liczb (szare obszary) dla przypadku dodatniej środkowej liczby.

Rys. 7 Konstrukcja optymalnego rozwiązania niezawierającego wyróżnionych trzech liczb (linia przerywana) na podstawie dowolnego rozwiązania optymalnego niezawierającego którejś z tych liczb (szare obszary) dla przypadku dodatniej środkowej liczby.

Pozostaje udowodnić poprawność operacji skracania. Powiedzmy, że ciąg składa się z 9 liczb oraz że liczbą o najmniejszej wartości bezwzględnej jest math (dodatnia), więc chcemy skrócić ciąg, zastępując math przez ich sumę (Rys. 7). Chcemy wykazać, że istnieje optymalne rozwiązanie, w którym każdy fragment albo zawiera wszystkie liczby math albo nie zawiera żadnej z nich. W tym celu pokażemy, jak z rozwiązania optymalnego niespełniającego tego warunku skonstruować rozwiązanie optymalne, które go spełnia. Rozważymy dwa przypadki:

  • (1) Rozwiązanie zawiera fragment, do którego należą dwie spośród liczb math Wyrzucając z tego fragmentu obie te liczby, dostaniemy nie gorsze rozwiązanie (bo math).
  • (2) Rozwiązanie zawiera jedną liczbę (czyli fragment math). Fragmentów jest mniej niż liczb dodatnich. Jeśli zatem jakaś z liczb dodatnich (powiedzmy math) nie należy do żadnego fragmentu, to zamieniając ją z  math dostaniemy nie gorsze rozwiązanie (bo math). W przeciwnym przypadku co najmniej jeden fragment zawiera więcej niż jedną liczbę (powiedzmy math). Możemy zatem wyrzucić z niego jedną liczbę ujemną math rozbijając go na dwa fragmenty. Wyrzucając również fragment math znowu dostaniemy nie gorsze rozwiązanie (bo math).

Dowód, gdy liczba o najmniejszej wartości bezwzględnej jest ujemna, jest symetryczny i zostawiamy go jako ćwiczenie dla Czytelników.

I wreszcie nadszedł czas na obiecaną wskazówkę do dowodu poprawności algorytmów A4A6. Wykonujmy kolejne fazy tych algorytmów, do momentu, aż wszystkie fragmenty będą zawierać liczby o tych samych znakach. Następnie wykonajmy na tak uzyskanym ciągu algorytm A8. Porównując podział tablicy po math-tej fazie algorytmu A4 i przed math-tą od końca fazą algorytmu A8, możemy dojść do wniosku, że w zasadzie te algorytmy działają tak samo, tylko w odwrotnej kolejności. Szkoda, że na giełdzie nie można najpierw sprzedać, a potem kupić...