Денис Сапожников
Денис Сапожников Contact author via telegram ВШЭ Прикладная математика и информатика 23', ШАД Data Science 22'

ДП по подотрезкам

Тема не должна быть сложной. Главная идея - это хранить оптимальный ответ на подотрезке l…r, поэтому состояние будет dp[l][r] (и, возможно, ещё что-то) и обходить в порядке увеличения длины отрезка. Разберём несколько задач:

Удали до ПСП

Дана последовательность $s$ из скобок (, ), [ и ] длины n. Необходимо удалить как можно меньше скобок так, чтобы получилась правильная скобочная последовательность.

Решение

Для начала, стоит сразу понять, что это задача на дп по подотрекам. Это значит, что бы будем вычислять dp[l][r] - оптимальный ответ на задачу на подотрезке строки $l \ldots r$. Более детально:

Пусть dp[l][r] - это какой максимальной длины может быть ПСП на подотрезке $l \ldots r$.

Определение ПСП подсказывает нам, какие же переходы возможны:

  1. По второму пункту определения, если скобка s[l] и скобка s[r] - парные, то мы можем их убрать, то то есть можем перейти к состоянию dp[l+1][r-1] + 2.
  2. По третьему пункту определения, мы можем разбить отрезок l…r на два независимых подотрезка $l \ldots m$ и $m+1 \ldots r$, то есть перейти к состоянию $dp[l][m] + dp[m+1][r]$.

Среди всех возможных переходов нужно взять максимум, так как мы хотим сохранить как можно больше скобок.

\[dp[l][r] = \max \begin{cases} dp[l + 1][r - 1] + 2, & если\ s[l]\ и\ s[r]\ парные\ скобки \\ dp[l][m] + dp[m][r], & \forall m \in [l + 1; r - 1] \end{cases}\]
Код
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>
#include <algorithm>
#include <vector>
#include <string>

using namespace std;

bool is_match(char a, char b) {
    return a == '(' && b == ')' || a == '[' && b == ']';
}

int main() {
    string s;
    cin >> s;
    int n = s.size();
    vector<vector<int>> dp(n + 1, vector<int>(n + 1));

    // ()[]

    for (int len = 1; len <= n; ++len) {
        for (int i = 0, j = len - 1; j < n; ++i, ++j) {
            if (len == 1) {
                dp[i][j] = 0;
            }
            else if (len == 2) {
                if (is_match(s[i], s[j])) {
                    dp[i][j] = 2;
                }
                else {
                    dp[i][j] = 0;
                }
            }
            else {
                dp[i][j] = 0;
                if (is_match(s[i], s[j]))
                    dp[i][j] = dp[i + 1][j - 1] + 2;
                for (int m = i + 1; m <= j - 1; ++m)
                    dp[i][j] = max(dp[i][j], dp[i][m] + dp[m + 1][j]);
            }
        }
    }

    cout << dp[0][n - 1];
}

Замени до ПСП

Дана последовательность $s$ из скобок (, ), [ и ] длины n. Необходимо заменить как можно меньше скобок так, чтобы получилась правильная скобочная последовательность.

Решение

Идея аналогична предыдущей задаче, просто переходы будут чуть сложнее: мы сможем менять скобку на правильную на концах рассматриваемого подотрезка $l \ldots r$. То есть, возможны будут следующие переходы:

  1. Если s[l] и s[r] - парные скобки, то их можно отбросить
  2. Если s[l] - открывающая, то можно поменять s[r] до парной закрывающей
  3. Если s[r] - закрывающая, то можно поменять s[l] до парной открывающей
  4. Можно заменить s[l] и s[r] на парные скобки
  5. Можно разбить подстроку на две части и решить задачу для каждой из частей независимо
\[dp[l][r] = \min \begin{cases} dp[l + 1][r - 1], & если\ s[l]\ и\ s[r]\ парные\ скобки \\ dp[l + 1][r - 1] + 1, & если\ s[l]\ открывающая\ или\ s[r]\ закрывающая \\ dp[l + 1][r - 1] + 2 \\ dp[l][m] + dp[m][r], & \forall m \in [l+1;r-1] \end{cases}\]

Максимальный подпаллиндром

Дана строка $s$. Необходимо найти максимальную подпоследовательность, являющуюся палиндромом.

Решение

Пусть $dp[l][r]$ - это длина максимальной подпоследовательности-палиндрома на подотрезке $l \ldots r$. Как считать такую динамику?

\[dp[l][r] = \max \begin{cases} dp[l+1][r], \\ dp[l][r - 1], \\ dp[l+1][r-1] + 2, & если\ s[l] = s[r] \end{cases}\]

То есть мы либо отбрасываем один символ с какой-то из сторон, либо символы равны и тогда мы можем увеличить подпаллиндром на 2.

Количество бинарных деревьев поиска

Дан набор целых чисел a длины n. Необходимо вычислить, какое количество различных бинарных деревьев поиска можно построить, используя все числа из набора.
Напомню, что бинарным деревом поиска может называется дерево, у которого удовлетворяющее двум ограничениям:
1. У каждой вершины не больше двух детей
2. Если в вершине стоит число x, то в левом поддереве все числа должны быть строго меньше x, а в правом - не меньше x.

Решение

Идея 1. Нужно отсортировать массив a. По правилам бинарного дерева поиска если есть вершина, то слева от неё все элементы должны быть строго меньше x, а справа - не меньше, поэтому мы не можем расставлять значения в другом порядке.

Идея 2. Применить дп по подотрезкам. Пусть $dp[l][r]$ - количество бинарных деревьев поиска, которое мы можем построить на подотрзке $l \ldots r$. Как его вычислить? Необходимо выбрать корень - $m$-тый элемент, а далее обратиться к $dp[l][m-1]$ и $dp[m+1][r]$, перемножив два этих числа (так как мы можем независимо выбрать любое поддерво слева и любое поддерево справа). Таким образом, переходы динамики выглядят так:

\[dp[l][r] = \sum\limits_{m = l}^{r} dp[l][m - 1] \cdot dp[m + 1][r]\]

При этом оставим вам на подумать, какие $m$ можно выбрать (не все из-за правил бинарного дерева поиска).

Код

Haha. Classic.