Розбиття на паліндроми#11245

Розбиття на паліндроми (Розбір)

Нехай — вхідний рядок. Підрядок s_i~...~s_j є паліндромом, якщо виконується одна з наступних умов:

Нехай функція IsPal(i, j) повертає , якщо s_i ... s_j є паліндромом, і в іншому випадку. Значення IsPal(i, j) зберігаємо в комірках pal[i][j].

Нехай функція f(i, j) повертає мінімальну кількість розрізів, необхідних для паліндромного розбиття рядка s_i~...~s_j. Значення цієї функції будемо зберігати в комірках масиву dp[i][j]. Тоді відповіддю на задачу буде значення f(0, s.size() - 1).

Зазначимо, що f(i, i) = 0, тому що рядок з однієї букви вже є паліндромом.

Для обчислення f(i, j) розділимо підрядок s_i~...~s_j на дві частини: s_i~...~s_k і s_{k+1}~...~s_j~(i \le k < j). Рекурсивно розв'язуємо задачі на рядках s_i~...~s_k і s_{k+1}~...~s_j. Шукаємо таке значення k~(i \le k < j), для якого сума f(i, k) + f(k + 1, j) + 1 буде мінімальною. Доданок в сумі означає, що ми робимо один розріз між символами s_k та s_{k+1}. Таким чином, отримуємо наступне рекурентне співвідношення:

f(i, j) = \min_{i \le k < j} (f(i,k) + f(k + 1, j) + 1)

Приклад

Наприклад, для рядка "" з символів маємо:

f(1, 7) = \min_{1 \le k < 7} (f(1,k) + f(k + 1, 7) + 1)

Сума набуває найменшого значення при k = 3:

f(1, 7) = f(1, 3) + f(4, 7) + 1 = 0 + 0 + 1 = 1

Оскільки рядки "" і "" є паліндромами, то f(1, 3) = f(4, 7) = 0.

Реалізація алгоритму

Оголосимо константи, вхідний рядок та робочі масиви.

#define MAX 1001
#define INF 0x3F3F3F3F
int dp[MAX][MAX], pal[MAX][MAX];
string s;

Функція IsPal(i, j) перевіряє, чи є підрядок s_i~...~s_j паліндромом. Підрядок s_i~...~s_j є паліндромом, якщо s_i = s_j і s_{i+1}~...~s_{j-1} є паліндромом. Значення IsPal(i, j) зберігаємо в комірках масиву pal[i][j].

int IsPal(int i, int j)
{
  if (i >= j) return pal[i][j] = 1;
  if (pal[i][j] != -1) return pal[i][j];
  return pal[i][j] = (s[i] == s[j]) && IsPal(i + 1, j - 1);
}

Функція f(i, j) повертає мінімальну кількість розрізів, необхідних для паліндромного розбиття підрядка s_i~...~s_j.

int f(int i, int j)
{

Якщо i = j, підрядок s_i~...~s_j складається з одного символу, і розрізати її немає сенсу. Якщо i > j, підрядок s_i~...~s_j вважається порожнім, і розрізи також не потрібні.

  if (i >= j) return 0;

Якщо значення f(i, j) вже було обчислено раніше, повертаємо збережене значення з масиву dp[i][j].

  if (dp[i][j] != INF) return dp[i][j];

Якщо підрядок s_i~...~s_j~(i < j) є паліндромом, то розрізати її немає сенсу. В цьому випадку мінімальна кількість розрізів дорівнює .

  if (IsPal(i, j)) return dp[i][j] = 0;

Розділимо підрядок s_i~...~s_j на дві частини: s_i~...~s_k~(i \le k) і s_{k+1}~...~s_j~(k + 1 \le j). Далі рекурсивно розв'язуємо задачі для обох частин: s_i~...~s_k і s_{k+1}~...~s_j. Шукаємо таке значення k~(i \le k < j), для якого сума f(i, k) + f(k + 1, j) + 1 найменша.

  for (int k = i; k < j; k++)
    dp[i][j] = min(dp[i][j], f(i, k) + f(k + 1, j) + 1);

Повертаємо відповідь dp[i][j] = f(i, j).

  return dp[i][j];
}

Основна частина програми. Читаємо вхідний рядок.

cin >> s;

Ініціалізуємо масиви.

memset(dp, 0x3F, sizeof(dp));
memset(pal, -1, sizeof(pal));

Обчислюємо і виводимо відповідь.

res = f(0, s.size() - 1);
cout << res << endl;