Informatyczny kącik olimpijski
Wypisz napis
W tym kąciku omówimy pierwsze zadanie z finału konkursu Google Code Jam 2010.
Zadanie 1. Naszym celem jest wypisanie kolejno liter tego napisu od lewej do prawej. Mamy do dyspozycji stempel, który możemy wyobrazić sobie jako stos, umożliwiający wykonywanie trzech typów operacji: doczepienie plakietki z literą lub odczepienie plakietki znajdującej się na wierzchu oraz przyłożenie stempla do kartki, w wyniku czego nadrukowuje się na niej litera z wierzchniej plakietki. Chcemy wypisać za pomocą najmniejszej możliwej liczby operacji, przy czym zaczynamy i kończymy z pustym stemplem (stosem).
Jednym z pierwszych pomysłów na rozwiązanie może być programowanie dynamiczne. Wyznaczamy w nim kolejne komórki tablicy oznaczające, w jakiej minimalnej liczbie operacji można wypisać początkowy fragment napisu, tak aby na końcu otrzymać stos reprezentowany przez napis (nad alfabetem ). Wynikiem jest wówczas Przy tym podejściu nieuchronnie napotykamy problem, że liczba możliwych stosów jest naprawdę duża. Zauważmy, że nigdy na stosie nie opłaca nam się trzymać więcej niż liter, gdyż wówczas musielibyśmy łącznie wykonać ponad operacji, a w operacjach naprawdę łatwo wypisać cały napis Czyli mamy co najwyżej różnych stosów, ale do tego jeszcze mnóstwo możliwych przejść z każdego stanu do stanów postaci
Pokażemy teraz, jak trochę polepszyć to rozwiązanie. Przede wszystkim na stosie ewidentnie nie opłaca się nigdy trzymać dwóch takich samych liter pod rząd. To redukuje liczbę interesujących nas stosów do Możemy też istotnie zmniejszyć liczbę rozważanych przejść, jeśli zauważymy, że w danym stanie wystarczy próbować dołożyć na stos co najwyżej jedną nową literkę. Skrótowe uzasadnienie na przykładzie: zamiast w danym stanie dokładać na stos literki , wystarczy dołożyć samą literkę . Jeśli potem, po zdjęciu , będę chciał użyć tego , którego nie dołożyłem, to dokładam to właśnie wtedy. Liczba operacji nie zwiększyła się, a postąpiłem jakby sprytniej. W ten sposób zmniejszyliśmy liczbę przejść z danego stanu do (dodanie co najwyżej jednej literki i usunięcie dowolnie wielu literek), co daje już rozwiązanie o koszcie czasowym Wyraźnie lepiej niż poprzednio, ale wciąż jakoś wolno.
Spróbujmy więc zmienić podejście do rozwiązania. Przyjrzyjmy się sytuacji początkowej. Zaczynamy od pustego stosu, a chcemy wypisać literkę więc musimy ją włożyć na stos. Oznaczmy przez „moment”, po którym zostanie zdjęte ze stosu, tzn. załóżmy, że znajduje się na spodzie stosu podczas wypisywania liter fragmentu po czym zostaje zdjęte ze stosu ( ). Podzieliliśmy zatem wyjściowy problem na dwa niezależne podproblemy, polegające na wypisaniu każdego z fragmentów oraz tak aby na końcu zdjąć ze stosu wszystkie dołożone po drodze litery. Jedyna różnica między podproblemami polega na tym, że podczas wypisywania możemy skorzystać z faktu, iż na stosie jest już litera To prowadzi nas do innego rozwiązania opartego na programowaniu dynamicznym. Przez oznaczmy minimalną liczbę operacji potrzebnych do wypisania fragmentu tak aby nie pozostawić na końcu żadnych dodatkowych liter na stosie. A to wszystko przy założeniu, że na szczycie stosu przed rozważeniem tego fragmentu znajduje się literka ; oznacza, że przed rozpoczęciem wypisywania stos był pusty. Dodajmy dla jasności, że literki ani niczego poniżej, nie możemy tu usunąć ze stosu. Przykładowo, wartości postaci obliczamy następująco: jeśli to a w przeciwnym razie
Przy tym dla Takie rozwiązanie ma złożoność czasową – pozwalało to zdobyć częściowe punkty na zawodach.
Istnieje zatem jeszcze lepsze rozwiązanie. Co ciekawe, aby je uzyskać, należy wrócić do poprzedniego, wykładniczego rozwiązania, i jeszcze dokładniej przyjrzeć się możliwym zawartościom stosu. Kluczowa obserwacja to: można nie rozważać stosów, w których występuje fragment . Faktycznie, rozważmy rozwiązanie, w którym na stosie pojawia się fragment tej postaci, i przyjrzyjmy się chwili, w której umieszczamy na stosie drugą z literek . Zróbmy jednak inaczej: zamiast umieszczać drugie , usuńmy ze stosu literkę . Liczba operacji nie zmieniła się, a na szczycie stosu wciąż mamy . Dalej wykonujemy rozwiązanie bez zmian, aż do chwili, w której próbuje ono zdjąć ze stosu to drugie , którego teraz nie ma. W zamian włóżmy na stos literkę . Liczba operacji nie zmieniła się, układ liter na stosie również. W ten sposób pozbyliśmy się jednego wystąpienia fragmentu ; jeśli jest ich więcej, eliminujemy je w ten sam sposób.
Podobnie można uzasadnić, że możemy nie rozważać stosów zawierających fragment dla dowolnych, różnych liter i Stosów bez takich napisów jest już bardzo mało: są to tylko stosy postaci ..., ... i jeszcze czterech innych (dowolna permutacja liter , , na początku). Tak oto tablica skurczyła się nam do rozmiaru Wreszcie liczbę przejść z każdego stanu możemy zmniejszyć do stałej, korzystając ze spostrzeżenia, że jeśli przy wypisywaniu decydujemy się na usuwanie liter ze stosu, to możemy to robić do chwili, gdy po raz pierwszy na szczycie stosu znajdzie się litera lub gdy stos stanie się pusty. To oznacza, że w danym stanie usuniemy ze stosu co najwyżej dwie literki (lub wszystkie) i dodamy na stos co najwyżej jedną. W ten sposób uzyskaliśmy rozwiązanie wzorcowe, o złożoności czasowej