Informatyczny kącik olimpijski
Melodia
Tym razem omówimy zadanie z pogranicza informatyki i muzykologii, które pojawiło się na Bałtyckiej Olimpiadzie Informatycznej w bieżącym roku.
W zadaniu mamy do czynienia z pewnym instrumentem muzycznym, za
pomocą którego można zagrać
rodzajów nut. Muzyk ma zadaną
melodię, którą chciałby zagrać – jest to sekwencja
kolejnych nut.
Niestety, nie jest to takie proste. Otóż zagranie każdej nuty wymaga zakrycia
niektórych otworów instrumentu, a jeśli dwie nuty istotnie różnią się
wymaganym sposobem zakrycia, to muzyk nie jest w stanie zagrać jednej
z tych nut bezpośrednio po drugiej... Innymi słowy, dla każdej pary nut
wiadomo z góry, czy można zagrać je pod rząd, czy też nie. Z tego względu
zagranie zadanej melodii może nie być możliwe. Muzyk chciałby więc
zagrać melodię możliwie najbliższą zadanej, to znaczy zagrać taką
melodię złożoną z
nut, która będzie różniła się od zadanej na
najmniejszej możliwej liczbie pozycji – i my mamy mu w tym pomóc.
Dodajmy, że w naszym zadaniu liczba możliwych nut jest stosunkowo
niewielka (rzędu 100), natomiast cała melodia może składać się nawet ze
nut.
Jednym z pierwszych pomysłów, jaki narzuca się po przeczytaniu tego
zadania, jest programowanie dynamiczne. Ponumerujmy poszczególne
rodzaje nut od
do
i oznaczmy kolejne nuty w zadanej
melodii przez
Niech dalej tablica wartości logicznych
reprezentuje podane pary nut, które można zagrać bezpośrednio
jedna po drugiej; zakładamy, że dla każdego
zachodzi
Chcemy wypełnić dwuwymiarową tablicę
w której
pole
oznacza najmniejszą liczbę błędów, jakie może popełnić
muzyk, grając – zamiast fragmentu melodii
– jakąś melodię
długości
kończącą się nutą typu
Pomijając warunki
brzegowe, pola tablicy
możemy wyznaczać za pomocą prostej
zależności rekurencyjnej, w której sprawdzamy wszystkie możliwe
rodzaje nut, jakie mogły wystąpić w melodii bezpośrednio przed zadaną
nutą:
![]() |
W powyższym wzorze
oznacza 1, jeśli
a 0 w przeciwnym
przypadku. To rozwiązanie działa w czasie
i pamięci
Da się jednak lepiej. Naturalne wydaje się skorzystanie z faktu, że powyższe
rozwiązanie oblicza jedynie
różnych wartości. Gdybyśmy
tylko byli w stanie jakoś sprytniej wyznaczać minimum we wzorze
np. w czasie stałym, to uzyskalibyśmy rozwiązanie działające
zadowalająco szybko. Pewną wskazówką jest tu pierwszy składnik występujący
we wzorze (*). Otóż, w przeciwieństwie do drugiego składnika, może on
przyjmować tylko dwie różne wartości, które zależą jedynie od tego, czy
w
-tym kroku decydujemy się zagrać poprawną nutę
czy
też nie.
Przyjrzyjmy się pierwszemu z tych przypadków i spróbujmy zmienić nieco
nasze podejście. Gdyby udało nam się wyznaczyć jednowymiarową tablicę
w której
to maksymalna liczba poprawnie zagranych
nut podczas wykonywania początkowego fragmentu melodii długości
przy założeniu, że
-tą nutę zagramy poprawnie, to
moglibyśmy już łatwo podać wynik – byłoby to maksimum ze wszystkich
elementów tablicy
(dlaczego?). Przy wypełnianiu tego typu tablic za
pomocą programowania dynamicznego
oblicza się zazwyczaj jako
maksimum z tych wartości
dla
które „pasują do
faktu
”. W naszym przypadku przy obliczaniu
bierzemy
pod uwagę te wartości
dla których istnieje poprawna (czyli
możliwa do zagrania) melodia długości
zawierająca na odpowiednich
pozycjach nuty
i
Nie dosyć, że powyższe rozwiązanie ma na starcie złożoność czasową
to jeszcze nie bardzo widać, jak takie
znajdować.
Jest do tego potrzebny pewien pomysł: otóż tablicę
możemy
potraktować jako macierz sąsiedztwa pewnego grafu. Wówczas wartość
pasuje do
jeśli w tym grafie istnieje ścieżka
długości
łącząca nuty
oraz
W tym celu
wystarczy sprawdzić, czy najkrótsza ścieżka w grafie od
do
ma długość nieprzekraczającą
(to przejście myślowe
wynika akurat z faktu, że
jest zawsze prawdziwe). Najkrótsze
ścieżki między wszystkimi parami wierzchołków (nut) możemy wyznaczyć
w czasie
czy to za pomocą algorytmu Floyda–Warshalla, czy to
wielokrotnie wykonując przeszukiwanie wszerz.
W ten sposób rozwiązaliśmy problem implementacji nowego rozwiązania, ale ciągle nie uporaliśmy się z jego paskudną złożonością czasową. Jedynym nowym pomysłem, jaki pojawił się po drodze, było wprowadzenie grafu nut. Jeśli zdecydujemy się poświęcić mu jeszcze trochę uwagi, to ta decyzja okaże się kluczowa dla skonstruowania efektywnego rozwiązania.
Potrzebne jest nam mianowicie już tylko następujące spostrzeżenie: graf nut ma
zaledwie
wierzchołków, więc wszystkie najkrótsze ścieżki w tym
grafie mają długości nieprzekraczające
To oznacza, że dla danego
wszystkie wartości
dla
są
z pewnością dobre, a zatem przy obliczaniu
wystarczy
tylko rozważyć elementy tablicy położone co najwyżej
pól
wstecz oraz maksimum ze wszystkich elementów położonych jeszcze
wcześniej!
Zanim jednak zaczniemy świętować nasz sukces, musimy poczynić
przykre spostrzeżenie, że graf nut może nie być spójny. To oznacza,
że dla danego
niektóre nawet bardzo odległe wartości
mogą nie być dobre. Przezwyciężenie tej trudności okazuje
się już tylko kwestią czasu. Możemy, na przykład, pamiętać zawsze
ostatnich elementów tablicy
a także maksima ze wszystkich
wcześniejszych elementów pogrupowane po poszczególnych nutach (czyli
grupujemy po
). Sprytniejsze rozwiązanie polega na
rozważeniu każdej spójnej składowej grafu nut z osobna i zastosowaniu dla
niej metody z jednym pamiętanym maksimum – wówczas nuty melodii
występujące poza składową traktujemy po prostu jako niemożliwe do
zagrania. W obu przypadkach otrzymujemy algorytm działający w czasie
i pamięci
Do tego musimy jeszcze doliczyć
koszt wyznaczenia najkrótszych ścieżek: czas
i pamięć
Dodajmy na koniec, że nasz muzyk nie będzie specjalnie zadowolony, jeśli podamy mu jedynie minimalną liczbę błędów, jakie może popełnić – chciałby przecież wiedzieć, jaką melodię powinien zagrać! Polecamy zatem Czytelnikowi zastanowić się nad tym, jak odtworzyć wynik.