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

Palindromiczny podział

Podział słowa jest palindromiczny, jeśli kawałki tworzące podział same tworzą palindrom. Np. dla słowa abbcdbab palindromicznym jest podział ab|b|cd|b|ab, bo $i$-ty od przodu i $i$-ty kawałek od tyłu są takie same.

Dane jest $n$-literowe słowo $s[0..n)$, dla którego należy obliczyć palindromiczny podział na największą liczbę kawałków.

Rozwiązanie kwadratowe

Jedno z najprostszych rozwiązań to proste programowanie dynamiczne. Przez $dp[i]$ oznaczamy maksymalną liczbę kawałków w palindromicznym podziale słowa $s[i .. n-i)$ (czyli bez pierwszych $i$ liter i bez ostatnich $i$ liter). Łatwo widać rekurencję:

$$dp[i] = \max_{j > i} (2 + dp[j]),$$ gdzie bierzemy pod uwagę takie wartości $j$, że $s[i .. j) = s[n-j .. n-i)$. Równość możemy sprawdzać brutalnie, wtedy dostaniemy rozwiązanie o złożoności $O(n^3)$:

const int N = 1100000;
int t,n,dp[N];
char s[N];

bool substr_equal(int i1, int i2, int len) {
  REP(i,len) {
    if (s[i1+i] != s[i2+i]) return false;
  }
  return true;
}

int main() {
  scanf("%d",&t);
  REP(test,t) {
    scanf("%s",s);
    n = strlen(s);

    dp[n/2] = n%2;
    for (int i=n/2-1; i>=0; --i) {
      dp[i] = 1;
      for (int j=i+1; j<=n/2; ++j) {
        if (substr_equal(i, n-j, j-i)) {
          dp[i] = max(dp[i], 2 + dp[j]);
        }
      }
    }

    printf("%d\n", dp[0]);
  }
}

Można je przyspieszyć do $O(n^2)$ przez użycie haszowania, dzięki któremu porównywanie słów można zrobić w czasie stałym:

void init_hasz() {
  pot[0] = 1;
  REP(i,n) {
    hasz[i+1] = (hasz[i] + ll(s[i])*pot[i]) % P;
    pot[i+1] = ll(pot[i])*A % P;
  }
}

int get_hasz(int i, int len) {
  return ll(hasz[i+len] + P - hasz[i]) * pot[n-i] % P;
}

bool substr_equal(int i1, int i2, int len) {
  return get_hasz(i1, len) == get_hasz(i2, len);
}

Rozwiązanie liniowe

Zadanie można rozwiązać, używając algorytmu zachłannego: wystarczy obcinać ze słowa jak najkrótsze kawałki palindromiczne (czyli najkrótsze prefiksy równe sufiksom). Jeśli użyjemy haszowania, taki program zadziała w czasie $O(n)$:

    int i=0, cnt=0, found=1;
    while (found) {
      found=0;
      for (int j=i+1; !found && j<=n/2; ++j) {
        if (substr_equal(i, n-j, j-i)) {
          i = j;
          cnt += 2;
          found = 1;
        }
      }
    }

    printf("%d\n", cnt + (i<(n+1)/2));

Dlaczego to rozwiązanie jest poprawne? Załóżmy, że w optymalnym rozwiązaniu skrajnym kawałkiem palindromicznym jest $w$, ale rozwiązanie zachłanne wybiera krótszy kawałek $v$. Pokażemy, że rozwiązanie zachłanne znajdzie nie gorszy podział.

Tak więc mamy równość $s = w \cdot s_1 \cdot w$, ale również $s = v\cdot s_2 \cdot v$. Jeśli więc $v$ jest nie dłuższe niż połowa długości $w$, to słowo $w$ musi zaczynać i kończyć się słowem $v$. Tak więc $w = v \cdot w_1 \cdot v$ i mamy palindromiczny podział $s = v \cdot w_1 \cdot v \cdot podz(s_1) \cdot v \cdot w_1 \cdot v$, który ma więcej kawałków niż optymalny, co daje sprzeczność.

Jeśli $v$ nie jest dłuższe niż połowa długości $w$, to oznacza, że słowo $w$ jest okresowe z okresem $|v|$. Tak więc dla pewnego podziału $v = v_1 v_2$ mamy $w = (v_1 \cdot v_2)^k \cdot v_1$. To znowu daje nam lepszy podział palindromiczny $s = (v_1 \cdot v_2)^k \cdot v_1 \cdot podz(s_1) \cdot (v_1 \cdot v_2)^k \cdot v_1.$