Omówienie zadania Kolorowy wąż (I etap XXX OI)
Treść zadania
Pomysł na zadanie wywodzi się z Gry w węża stworzonej na dawne telefony Nokia, w której wąż przemieszcza się po dwuwymiarowej planszy, po drodze zjadając przekąski. W każdej jednostce czasu wąż może poruszyć się w górę, w dół, w prawo lub w lewo, przy czym o kierunku ruchu decyduje gracz. Kiedy wąż zjada przekąskę, wydłuża się o pole, na którym znajdowała się przekąska. Po szczegóły dotyczące tego, jak przemieszcza się wąż w tej grze, odsyłamy do treści zadania.
W przeciwieństwie od oryginalnej Gry w węża, w naszym zadaniu rozmiar planszy (\(m\)) może być bardzo duży, podobnie jak liczba przekąsek (\(p\)). Co więcej, w zadaniu każda przekąska ma pewien kolor. Kiedy wąż zjada przekąskę, jego nowa głowa przejmuje kolor przekąski. Stąd tytuł naszego zadania, Kolorowy wąż.
Naszym zadaniem jest symulacja ruchu węża na planszy – czyli nie my musimy decydować, jakie ruchy wykonuje wąż, tylko mamy je już podane. Podczas symulacji musimy umieć odpowiadać na pytania, w których dostajemy współrzędne pola na planszy i mamy stwierdzić, czy na tym polu znajduje się jakiś fragment węża, a jeśli tak, jaki jest jego kolor. Podobnie jak liczba przekąsek, łączna liczba operacji (\(n\)), czyli ruchów węża i pytań, jest potencjalnie duża.
Oryginalna Gra w węża
Rozwiązanie zadania rozpoczniemy nietypowo, od podzadania 3, które naprowadzi nas na rozwiązanie wzorcowe. W tym podzadaniu, podobnie jak w oryginalnej Grze w węża, wszystkie przekąski mają ten sam kolor. Wówczas w odpowiedzi na pytanie wystarczy stwierdzić, czy na danym polu planszy znajduje się jakiś fragment węża, czy nie.
Kluczową kwestią jest zastanowić się nad najbardziej dogodną reprezentacją planszy i węża. Plansza ma wymiary \(m \times m\) dla \(m \le 2000\), więc możemy ją reprezentować w postaci boolowskiej tablicy dwuwymiarowej przekaski
. Założymy, że przekaski[i][j]=true
wtedy i tylko wtedy, gdy na polu w wierszu i
i kolumnie j
znajduje się przekąska.
Gdy wąż przesuwa się, zbiór zajmowanych przez niego pól zmienia się bardzo nieznacznie – dochodzi nowe pole, na którym znajdzie się głowa węża, a znika pole, na którym był koniec jego ogona – chyba że wąż zjada przekąskę, to wtedy ogon nie znika. Wąż funkcjonuje zatem jak struktura danych o nazwie kolejka.
Wykorzystamy strukturę danych queue
z języka C++, która udostępnia następujące metody:
pop()
: usuwa element z początku kolejkipush(x)
: wstawia elementx
na koniec kolejkifront()
: daje w wyniku element na początku kolejkiback()
: daje w wyniku element na końcu kolejki.
queue<pair<int, int> > waz
). W przypadku naszego węża, początek kolejki będzie odpowiadał ogonowi, a koniec głowie. Do odpowiadania na pytania przyda się boolowska tablica dwuwymiarowa gdzie_waz
reprezentująca zbiór pól zajmowanych przez węża. Wtedy ruch węża w przypadku, gdy, powiedzmy, użytkownik wykonuje ruch w górę, wygląda następująco:
auto glowa = waz.back();
if (znak == 'G') --glowa.first; // głowa przesuwa się w górę
else if (znak == 'D') ++glowa.first; // w dół
else if (znak == 'L') --glowa.second; // w lewo
else ++glowa.second; // w prawo: znak == 'P'
waz.push(glowa);
bool &przekaska = przekaski[glowa.first][glowa.second]; // referencja pozwala łatwo "wyzerować" przekąskę
if (przekaska) {
przekaska = false; // przekąska znika z planszy
} else {
auto ogon = waz.front();
waz.pop(); // ogon przesuwa się
gdzie_waz[ogon.first][ogon.second] = false;
}
gdzie_waz[glowa.first][glowa.second] = true;
Tak więc każdy ruch węża obsługujemy w czasie stałym. Na pytanie o dane pole odpowiadamy również w czasie stałym na podstawie komórki tablicy gdzie_waz
. Tablicę przekaski
wypełniamy w czasie \(O(m^2+p)\). Złożoność czasowa rozwiązania podzadania 3 to zatem \(O(m^2+p+n)\).
Rozwiązanie wzorcowe
Pola planszy zajmowane przez węża działają na zasadzie kolejki, natomiast kolory tych pól, w kierunku od ogona do głowy, działają jak stos. Aby uzyskać rozwiązanie wzorcowe, musimy węża reprezentować nie wprost, pamiętając osobno kolejkę i stos.
Aby odpowiadać na pytania, musimy jakoś zmodyfikować tablicę gdzie_waz
. Nie możemy sobie pozwolić na trzymanie w niej koloru fragmentu węża aktualnie zajmującego dane pole, jako że wtedy każdy ruch węża wymagałby wykonania tylu zmian w tej tablicy, ile wynosi aktualna długość węża. Łącznie otrzymalibyśmy rozwiązanie o złożoności \(\Omega(n^2)\), które miałoby szansę przejść tylko pierwsze podzadanie.
Zamiast tego możemy wpaść na pomysł, żeby gdzie_waz[i][j]
przechowywało moment, w którym wąż wszedł na pole w wierszu i
i kolumnie j
, o ile wąż się tam aktualnie znajduje. Na tej podstawie, znając aktualny moment oznaczony zmienną ruch
i mając do dyspozycji stos kolory
reprezentujący kolory pól węża, jesteśmy w stanie podać kolor fragmentu węża znajdującego się obecnie na tym polu.
Będziemy musieli być w stanie powiedzieć, jaki kolor znajduje się w danym miejscu stosu reprezentującego węża. Tej funkcjonalności nie zapewnia kontener stack
z C++. Do reprezentacji stosu należy użyć np. kontenera vector
, który wspiera nadzbiór operacji tego poprzedniego.
Poniżej umieszczamy kod odpowiadający za symulację ruchu głowy w górę. Zaznaczyliśmy w nim zmiany w stosunku do kodu dla jednokolorowego węża. W szczególności teraz tablica przekaski
zawiera kolor przekąski lub -1
, jeśli na danym polu nie ma przekąski.
auto glowa = waz.back();
if (znak == 'G') --glowa.first; // głowa przesuwa się w górę
else if (znak == 'D') ++glowa.first; // w dół
else if (znak == 'L') --glowa.second; // w lewo
else ++glowa.second; // w prawo: znak == 'P'
waz.push(glowa);
int &przekaska = przekaski[glowa.first][glowa.second]; // NOWE
if (przekaska != -1) {
kolory.push_back(przekaska); // NOWE; przekąska trafia na stos
przekaska = -1; // NOWE; przekąska znika z planszy
} else {
auto ogon = waz.front();
waz.pop(); // ogon przesuwa się
gdzie_waz[ogon.first][ogon.second] = -1; // NOWE
}
gdzie_waz[glowa.first][glowa.second] = ruch++; // NOWE, numer aktualnego ruchu
Aby odpowiedzieć na pytanie o kolor fragmentu węża znajdującego się w wierszu w
i kolumnie k
, możemy teraz użyć następującego fragmentu kodu.
if (gdzie_waz[w][k] == -1) {
cout << "-1\n";
} else {
int ktory_od_poczatku = ruch - gdzie_waz[w][k];
cout << kolory[(int)kolory.size() - 1 - ktory_od_poczatku] << '\n';
}
Złożoność czasowa rozwiązania wzorcowego to także \(O(m^2+p+n)\).
Opis przygotowano w ramach projektu "Mistrzostwa w Algorytmice i Programowaniu - Uczniowie" - zadania finansowanego ze środków Ministerstwa Cyfryzacji.