Opis
BOI 2016
Bosses
Park
Spiral
Cities
Maze
Swap
CEOI 2017
Skierowanie dróg
Pewny zakład
Pułapka Mariusza
Budujemy mosty
Pościg Mariusza
Palindromiczny podział
BOI 2018
Miłosny wielokąt
Marsjańskie DNA
Problemy dżdżownicy
Prąd przemienny
Genetyka
Ścieżki
BOI 2019
Flash Memory
Nautilus
Alpine Valley
Tom's Kitchen
Necklace
Olympiads
BOI 2020
Kolory
Mieszanki
Joker
Graf
Wioska
Wirusy
CEOI 2020
Fajny płot
Drogi
Star Trek
Napój Wielkiej Potęgi
Wiosenne porządki
Szachowa gorączka

Necklace

Treść zadania

Mamy dwa słowa s1 i s2 o długościach n1 i n2. Chcemy wybrać z nich jak najdłuższe podsłowa w1 i w2 tak, żeby były „naszyjnikowo równoważne”, czyli żeby w2 mogło powstać przez cykliczny obrót i/lub odwrócenie słowa w1.

Rozwiązanie sześcienne

Najpierw rozważmy trochę prostszy problem słów „cyklicznie równoważnych”. Wtedy istnieje podział w1=uv oraz w2=vu. Możemy rozważyć wszystkie możliwe pozycje kropek w tych podziałach w słowach s1 i s2. Oznaczmy je przez i oraz j:

               i
  s_1: ....UUUU|VVV....
            j
  s_2: ..VVV|UUUU......

Teraz osobno rozważymy każdą długość słowa u i dla każdej z nich sprawdzimy równość fragmentów s1[i..i)=s2[j..j+). Analogicznie zrobimy dla słów v. Na koniec wybierzemy odpowiedź dającą największą sumę długości.

Złożoność będzie wynosić O(n3), jeśli będziemy umieli w czasie stałym sprawdzić równość słów. W tym celu można użyć haszowania (co da obliczenia wstępne w czasie O(n)), ale ponieważ możemy sobie pozwolić na czas O(n2), to łatwiej nam będzie obliczyć tablicę lcp[i][j], która oznacza największe , że mamy równość s1[i..i+)=s2[j..j+). To robimy bardzo prostym programowaniem dynamicznym.

Na koniec powróćmy do problemu „równoważności naszyjnikowej”. Tu przyda nam się techniczna sztuczka: wystarczy jeszcze raz wykonać algorytm równoważności cyklicznej dla słowa s1 oraz odwróconego słowa s2 (trzeba jedynie pamiętać o odpowiednim wzięciu indeksu w odwróconym słowie).

Kod wygląda następująco i zalicza dwa podzadania:

const int N = 3100;
char s1[N],s2[N];
int n1,n2;
int lcp[N][N];
int ans=0, ai, aj;

void solve(bool rev) {
  for(int i=n1-1; i>=0; --i) {
    for(int j=n2-1; j>=0; --j) {
      if (s1[i] == s2[j]) {
        lcp[i][j] = 1 + lcp[i+1][j+1];
      } else {
        lcp[i][j] = 0;
      }
    }
  }

  REP(i,n1+1) REP(j,n2+1) {

    int len_a=0, len_b=0;
    REP(len,i+1) {
      if (lcp[i-len][j] >= len) {
        len_a = len;
      }
    }
    REP(len,j+1) {
      if (lcp[i][j-len] >= len) {
        len_b = len;
      }
    }

    if (len_a + len_b > ans) {
      ans = len_a + len_b;
      ai = i-len_a;
      aj = rev ? n2-j-len_a : j-len_b;
    }
  }
}

int main() {
  scanf("%s%s",s1,s2);
  n1 = strlen(s1);
  n2 = strlen(s2);

  solve(false);
  reverse(s2,s2+n2);
  solve(true);

  printf("%d\n%d %d\n", ans, ai, aj);
}

Rozwiązanie kwadratowe

Spróbujemy szybciej wyznaczać wartości lenb[i][j], czyli długość słowa v, dla pozycji i oraz j. Dla ustalonej pozycji i będziemy przechodzić po wszystkich pozycjach j słowa s2. Ponieważ wiemy, że mamy równość fragmentów s1[i..i+)=s2[j..j+), dla =lcp[i][j], to widać, że dla j=j+1,j+2,,j+ mamy lenb[i][j]jj.

Możemy zatem przejść po wszystkich takich fragmentach i utrzymywać najdłuższy „daszek”, który ciągle pokrywa naszą pozycję j:

  REP(i,n1+1) {
    int idx = -1;
    REP(j,n2+1) {
      while (idx == -1 || idx < j && idx + lcp[i][idx] <= j) {
        idx++;
      }
      int len = 0;
      if (idx + lcp[i][idx] > j) {
        len = j - idx + 1;
      }
      cache_len_b[i][j+1] = len;
    }
  }

  REP(j,n2+1) {
    int idx = -1;
    REP(i,n1+1) {
      while (idx == -1 || idx < i && idx + lcp[idx][j] <= i) {
        idx++;
      }
      int len = 0;
      if (idx + lcp[idx][j] > i) {
        len = i - idx + 1;
      }
      cache_len_a[i+1][j] = len;
    }
  }

Analogicznie wyznaczamy tablicę lena. Ten algorytm będzie działał w czasie O(n2) i rozwiąże podzadanie 3.

Pamięć liniowa

Poprzednie rozwiązanie wymaga pamięci O(n2), ale można je jeszcze poprawić, aby zużywało tylko O(n) pamięci.

Odpowiadanie na zapytania o LCP można zrealizować haszowaniem w pamięci liniowej.

Iterujemy się po wszystkich wartościach i. Dla każdej z nich liczymy w pamięci liniowej wiersz tablicy lenb[i][].

Po przeorganizowaniu kodu liczącego tablicę lena (np. odwróceniu obu słów s1 i s2 i uruchomieniu algorytmu jak dla lenb), również możemy wyznaczyć jeden wiersz lena[i][].

Dzięki temu odzyskamy odpowiedź dla ustalonej pozycji i.