Przeskocz do treści

Delta mi!

Informatyczny kącik olimpijski

Wypisz napis

Jakub Radoszewski

o artykule ...

  • Publikacja w Delcie: wrzesień 2011
  • Publikacja elektroniczna: 31-08-2011
  • Wersja do druku [application/pdf]: (38 KB)

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ą math lub  math 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ć math 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 math oznaczające, w jakiej minimalnej liczbie operacji można wypisać początkowy fragment napisu, math  tak aby na końcu otrzymać stos reprezentowany przez napis math (nad alfabetem math). Wynikiem jest wówczas math 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ż math liter, gdyż wówczas musielibyśmy łącznie wykonać ponad math operacji, a w math operacjach naprawdę łatwo wypisać cały napis math  Czyli mamy co najwyżej math różnych stosów, ale do tego jeszcze mnóstwo możliwych przejść z każdego stanu math do stanów postaci  math

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 math 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  math math, wystarczy dołożyć samą literkę  math. Jeśli potem, po zdjęciu  math, będę chciał użyć tego  math, którego nie dołożyłem, to dokładam to  math 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 math  (dodanie co najwyżej jednej literki i usunięcie dowolnie wielu literek), co daje już rozwiązanie o koszcie czasowym math 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ę math  więc musimy ją włożyć na stos. Oznaczmy przez math „moment”, po którym math  zostanie zdjęte ze stosu, tzn. załóżmy, że math  znajduje się na spodzie stosu podczas wypisywania liter fragmentu math  po czym zostaje zdjęte ze stosu ( math). Podzieliliśmy zatem wyjściowy problem na dwa niezależne podproblemy, polegające na wypisaniu każdego z fragmentów math oraz math  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 math  możemy skorzystać z faktu, iż na stosie jest już litera math  To prowadzi nas do innego rozwiązania opartego na programowaniu dynamicznym. Przez math oznaczmy minimalną liczbę operacji potrzebnych do wypisania fragmentu math  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 math; math  oznacza, że przed rozpoczęciem wypisywania math stos był pusty. Dodajmy dla jasności, że literki math  ani niczego poniżej, nie możemy tu usunąć ze stosu. Przykładowo, wartości postaci math obliczamy następująco: jeśli math  to  math a w przeciwnym razie

display-math

Przy tym math dla math Takie rozwiązanie ma złożoność czasową math  – 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 math math math. 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  math. Zróbmy jednak inaczej: zamiast umieszczać drugie  math, usuńmy ze stosu literkę  math. Liczba operacji nie zmieniła się, a na szczycie stosu wciąż mamy  math. Dalej wykonujemy rozwiązanie bez zmian, aż do chwili, w której próbuje ono zdjąć ze stosu to drugie  math, którego teraz nie ma. W zamian włóżmy na stos literkę  math. Liczba operacji nie zmieniła się, układ liter na stosie również. W ten sposób pozbyliśmy się jednego wystąpienia fragmentu math math math; 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 math dla dowolnych, różnych liter math i math Stosów bez takich napisów jest już bardzo mało: są to tylko stosy postaci math math math math math math..., math math math math math math... i jeszcze czterech innych (dowolna permutacja liter mathmathmath na początku). Tak oto tablica math skurczyła się nam do rozmiaru math  Wreszcie liczbę przejść z każdego stanu możemy zmniejszyć do stałej, korzystając ze spostrzeżenia, że jeśli przy wypisywaniu math  decydujemy się na usuwanie liter ze stosu, to możemy to robić do chwili, gdy po raz pierwszy na szczycie stosu znajdzie się litera math  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 math