Przeskocz do treści

Delta mi!

Kwadraty

Jakub Radoszewski

o artykule ...

  • Publikacja w Delcie: marzec 2011
  • Publikacja elektroniczna: 02-03-2011

Tym razem zajmiemy się trochę innymi kwadratami niż zazwyczaj. Chodzi mianowicie o napisy postaci math czyli sklejenie jakiegoś słowa (ciągu liter) math z nim samym. Przykładowymi kwadratami występującymi w języku polskim są słowa mama, kankan, rowerowe, wałowało, esemesem.

obrazek

Jeśli rozważamy jakieś słowo, może nas interesować, czy jest ono kwadratem, ale także czy jakieś jego podsłowo (tzn. spójny fragment) jest kwadratem. Jeżeli nie, to słowo takie nazywamy bezkwadratowym. Zastanówmy się przez chwilę nad tym, jak konstruować słowa bezkwadratowe.

Najprościej wybrać słowo, w którym wszystkie litery są różne, np. abcde... Gdyby liter w alfabecie było nieskończenie wiele, to moglibyśmy w ten sposób skonstruować dowolnie długie słowo bezkwadratowe. Nie da się ukryć, że nie jest to zbyt ciekawy przykład. No to może spróbujmy wygenerować długie słowo bezkwadratowe nad jakimś mniejszym alfabetem?

Alfabet jednoliterowy na pewno nam nie pomoże. Załóżmy więc, że mamy do dyspozycji dwie litery, a i b. Widać, że każde dwie kolejne litery w słowie bezkwadratowym muszą być różne, a zatem nasze słowo musi zaczynać się jakoś tak: aba... lub bab... Niestety, w obu tych przykładach nie możemy dołożyć już żadnej litery, gdyż wówczas otrzymamy kwadrat math lub odpowiednio math To oznacza, że najdłuższe słowo bezkwadratowe nad alfabetem dwuliterowym ma tylko trzy litery.

Kolejna próba: alfabet trzyliterowy. Znów konstruujemy słowo, biorąc zawsze kolejną literę różną od poprzedniej. Daje to zawsze dwie możliwości wyboru. W takim razie dodajmy warunek, że każda kolejna litera musi być różna od środkowej litery dotychczasowego słowa – dokładniej, przy wyznaczaniu math-tej litery interesuje nas litera o indeksie math przy czym litery słowa numerujemy od zera. Jeżeli to kryterium wciąż dopuszcza dwie możliwości, to wybieramy tę spośród niezabronionych liter, która występuje wcześniej w alfabecie. W ten sposób otrzymujemy takie oto słowo:

display-math

Okazuje się, że dowolnie długie słowo wygenerowane w ten sposób jest bezkwadratowe. Można ten fakt udowodnić formalnie, jednak w tym artykule zastosujemy podejście informatyczne natury eksperymentalnej: weźmiemy odpowiednio długie słowo tej postaci (np. złożone z miliona liter) i sprawdzimy za pomocą programu komputerowego, czy jest w tym słowie jakieś kwadratowe podsłowo. Jeśli okaże się, że nie, to zapewne wszystkie takie słowa są bezkwadratowe...

Nie jest wcale łatwo zaproponować efektywny, a zarazem nieskomplikowany algorytm sprawdzający, czy dane słowo jest bezkwadratowe – zachęcamy Czytelnika do próby samodzielnego zmierzenia się z tym problemem. Poniżej przedstawiamy elegancki algorytm o złożoności czasowej math przy czym math to długość badanego słowa math wzorowany na trudno dostępnej i trochę zapomnianej pracy M. Maina i R. Lorentza sprzed 25 lat.

Zacznijmy od prostego sprawdzenia, czy w słowie math znajduje się jakaś para równych kolejnych liter – to eliminuje nam kwadraty słów długości 1. W głównej części algorytmu wykonujemy math kroków; w math-tym kroku (dla math) sprawdzamy, czy słowo math zawiera podsłowo kwadratowe math takie że długość math (oznaczenie: math) należy do przedziału domknięto-otwartego math Wykonując taki krok, zakładamy, że math nie zawiera podsłów kwadratowych o długości połówki krótszej niż rozważane w tym kroku. Naszym celem jest wykonanie każdego kroku w złożoności czasowej  math

Poszukiwania żądanego kwadratu rozpoczynamy od podziału słowa math na bloki długości math (jeżeli nie dzieli się równo, to końcowej, krótszej grupy liter nie rozpatrujemy). Zauważmy, że jeżeli w math występuje kwadrat math taki że math to pierwsze wystąpienie math w ramach math musi zawierać co najmniej jeden z bloków podziału. Oznaczmy ten blok przez math To samo podsłowo pojawia się także na pozycji math słowa math choć to drugie wystąpienie nie musi już być blokiem podziału.

W naszym algorytmie rozważamy każdy kolejny blok math i poszukujemy wszystkich jego wystąpień w math zaczynających się na pozycjach z przedziału math Co ciekawe, takie wystąpienia mogą być co najwyżej dwa. Faktycznie, żadne dwa wystąpienia math w ramach math nie mogą na siebie nachodzić ani nawet się stykać, gdyż wówczas wyznaczałyby one kwadrat słowa o długości nie większej niż math (dlaczego?). Stanowiłoby to sprzeczność z założeniem, że math nie zawiera kwadratu krótszego niż math

obrazek

Dla każdego wystąpienia math w math na pozycji math musimy jakoś sprawdzić, czy wystąpienia z pozycji math oraz math wyznaczają jakiś kwadrat math taki że math Poszukując takiego kwadratu, wystarczy skupić się na badaniu równości par liter słowa math o indeksach oddalonych o  math Najpierw sprawdzamy, czy math math i tak dalej, aż natrafimy na parę różnych liter albo aż dalszym indeksem dojdziemy do pozycji math co oznacza, że znaleźliśmy kwadrat. Następnie powtarzamy to postępowanie, ale tym razem idąc do przodu, tzn. sprawdzamy, jak długo zachodzi math dla math Tym razem możemy zatrzymać się, jeśli liczba wykonanych tutaj kroków powiększona o liczbę kroków wykonanych wcześniej jest nie mniejsza niż math patrz rysunek. Jeżeli nie dojdziemy do wartości math to łatwo zauważyć, że rozważana para wystąpień podsłowa math nie wyznacza kwadratu.

Na tym rozumowaniu oparty jest poniższy pseudokod algorytmu wykrywania kwadratu w słowie math

function CzyJestKwadrat(s, n) 
  for  math to  math do 
     if  math then return true
    math 
   while  math do 
       math 
      while  math do 
          math  wystąpienia  math zaczynające się 
                   na pozycjach z przedziału [j + 2l, j + 4l); 
          for each  math do 
             lewo := długość najdłuższego wspólnego sufiksu słów  math i math 
             prawo := długość najdłuższego wspólnego prefiksu 
                             słów math i math 
             if lewo + prawo  math  then return true
           math 
       math 
   return false
end function

Zastanówmy się nad złożonością czasową tego algorytmu, przy okazji uzupełniając szczegóły techniczne jego implementacji. Pierwszym interesującym miejscem jest wyznaczanie zbioru  math Znane są różne efektywne algorytmy wyszukiwania wzorca (u nas jest to słowo  math) w tekście (u nas: zadany fragment słowa  math), np. algorytmy Knutha–Morrisa–Pratta, Boyera–Moore’a itp. W tym miejscu czeka nas jednak kolejne zaskoczenie: otóż w naszym programie w ogóle nie musimy używać żadnego z tych wysublimowanych algorytmów! Zaczynamy od sprawdzenia, literka po literce, czy math pasuje do math od pozycji math Jak w pewnym momencie zakończymy to sprawdzanie (albo znajdując wystąpienie math albo wskutek natrafienia na parę różnych liter na odpowiadających pozycjach), to kolejną próbę przypasowania słowa math wykonujemy od pierwszej pozycji w math następującej za wszystkimi przejrzanymi. Faktycznie, wystąpienie słowa math w math nie może nachodzić na żadne inne wystąpienie niepustego prefiksu słowa math w math gdyż wówczas math zawierałoby kwadrat jakiegoś prefiksu słowa math a przecież math W ten sposób znajdujemy szukane co najwyżej dwa elementy zbioru math w czasie math

Kolejny ciekawy moment to wyznaczanie wartości lewo prawo, wykonywane troszkę inaczej niż w opisie słownym algorytmu. Uzasadnienie poprawności pomijamy, przyjrzyjmy się kwestii złożoności czasowej. Łączna liczba operacji wykonywanych tutaj może być całkiem duża. Zauważmy jednak, że jeśli przy wyznaczaniu wspólnego sufiksu i prefiksu wykonamy więcej niż math operacji, to na pewno zaraz potem zakończymy działanie algorytmu, więc możemy sobie ten jeden raz pozwolić na wykonanie nawet i rzędu math operacji. W przeciwnym razie liczba tych operacji nie przekroczy math która to wartość – przypomnijmy – jest nie większa niż math czyli jest rzędu math

Widzimy zatem, że wnętrze wewnętrznej pętli while wykonujemy – poza ewentualnie jedynym jej obrotem, kończącym cały algorytm – w czasie math Pętla ta wykonuje math obrotów, co pokazuje, że koszt czasowy jednego obrotu zewnętrznej pętli while to mathTa, z kolei, wykonuje co najwyżej math obrotów, skąd wnosimy, że rzeczywiście opisany algorytm ma złożoność czasową math

Na koniec pytanie do Czytelnika: czy można ten algorytm jakoś łatwo przerobić, tak aby wykrywał wszystkie kwadraty w słowie?