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

ДП по подмножествам и цифрам. Разбор задач

Задача (скука)

Задана последовательность a, состоящая из n целых чисел. Игрок может сделать несколько ходов. За один ход игрок может выбрать некоторый элемент последовательности (обозначим выбранный элемент ak) и удалить его, при этом из последователости также удаляются все элементы, равные ak+1 и ak - 1. Описанный ход приносит игроку ak очков. Какое максимальное количество очков возможно набрать?

Решение

Обозначим за cnt[x] количество элементов, равных x. Заметим, что если мы хотя бы раз выбрали элемент, равный x, то мы можем выбрать все cnt[x] элементов, равных x.

Далее пусть dp[x] - максимальное количество очков, которое можно набрать, если бы в массиве были элементы, не превосходящие x. Тогда переходы динамики очень простые:

dp[i] = max(dp[i - 1], dp[i - 2] + cnt[i] * i)

То есть мы либо берем все числа, равные i и тогда не можем взять числа, равные i-1, либо берем оптимальный ответ для i-1, не взяв при этом i числа, равные i.

Код

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 1e5 + 1;

int main() {
    int n;
    cin >> n;
    vector<int> cnt(N);
    for (int i = 0; i < n; ++i) {
        int x;
        cin >> x;
        cnt[x]++;
    }
    vector<long long> dp(N);
    dp[0] = 0;
    for (int i = 1; i < N; ++i) {
        dp[i] = max(dp[i - 1], cnt[i] * 1LL * i + (i == 1 ? 0 : dp[i - 2]));
    }
    cout << max(dp[N - 1], dp[N - 2]);
}

Задача (гирьки: две одинаковые кучки равной массы)

Дан набор гирек массой m1,…,mN. Разделите этот набор на две кучки равной массы, содержащие равное число гирек.

Решение

Здесь нужно совсем чуть-чуть модифицировать рюкзак, а именно: пусть dp[i][w][k] - обозначает, можем ли мы набрать среди первых i гирек рюкзак массой w, содержащий k гирек. Переходы будут иметь вид: (i, w, k) -> (i + 1, w, k) и (i + 1, w + weight[i], k + 1). Ответ будет лежать в dp[n][w/2][k/2].

Код

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <iostream>
#include <vector>

using namespace std;

const int N = 100;
const int A = 100;
bool dp[N + 1][N * A][N + 1];

int main() {
    int n;
    cin >> n;
    vector<int> a(n);
    int sum_w = 0;
    for (int& x : a) {
        cin >> x;
        sum_w += x;
    }

    if (n % 2 || sum_w % 2) {
        cout << -1;
        return 0;
    }

    dp[0][0][0] = true;
    for (int i = 0; i < n; ++i) {
        for (int w = 0; w < n * A; ++w) {
            for (int j = 0; j <= i; ++j) {
                // dp[i][w][j] -> dp[i + 1][w][j], dp[i + 1][w + a[i]][j + 1]
                dp[i + 1][w][j] |= dp[i][w][j];
                dp[i + 1][w + a[i]][j + 1] |= dp[i][w][j];
            }
        }
    }

    if (!dp[n][sum_w / 2][n / 2]) {
        cout << -1;
        return 0;
    }

    int cur_w = sum_w / 2;
    int cur_k = n / 2;
    int i = n;

    vector<int> ans1, ans2;
    while (i > 0) {
        if (!dp[i - 1][cur_w][cur_k]) { // dp[i][cur_w][cur_k]
            ans1.push_back(i);
            cur_w -= a[i - 1];
            cur_k--;
        }
        else {
            ans2.push_back(i);
        }
        i--;
    }

    for (int x : ans1)
        cout << x << ' ';
    cout << '\n';
    for (int x : ans2)
        cout << x << ' ';
}

Задача (максимальное число)

Найдите число из отрезка [a, b] с максимальным произведением цифр.

Часто нужно перебрать все числа из какого-то большого интервала и посчитать количество чисел, котрое удовлетворяет какому-то условию, либо найти “лучшее” среди чисел в этом интервале. Тогда нам поможет стандартное ДП по цифрам.

Решение

Пусть dp[l][f1][f2] - это максимальное произведение цифр для числа длины ровно l. При этом если f1 = 0, то “оптимальное” число длины l строго меньше префикса длины l числа a, если f1 = 1, то префиксы совпадают и если f1 = 2, то префикс строго больше. Аналогично для f2 и числа b.

Зачем такие извращения? Потому что по такому состоянию очень легко узнать, находится ли число в интервале [a; b] или нет. Кроме того, переходы тоже очень легко считаются: если f1 = 0, то переход вохможен только в состояния с f1 = 0 (то же самое для f1 = 2 и для f2). Если же f1 = 1, то вы можете перейти в любое состояние в зависимости от очередной цифры, которую вы сейчас поставите.

Код

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <iostream>
#include <vector>
#include <string>
#include <cstring>

using namespace std;

const int N = 20;
long long dp[N + 1][3][3];
string opt[N + 1][3][3];

int get_nf(int f, int new_d, char sd) {
    sd -= '0';
    if (f == 0 || f == 2)
        return f;
    if (new_d < sd)
        return 0;
    if (new_d == sd)
        return 1;
    if (new_d > sd)
        return 2;
}

bool is_ok(int l, int lena, int lenb, int f1, int f2) {
    bool ok1 = (l == lena && (f1 == 2 || f1 == 1)) || l > lena; // x >= a
    bool ok2 = (l == lenb && (f2 == 1 || f2 == 0)) || l < lenb; // x <= b
    return ok1 && ok2;
}

int main() {
    long long a, b;
    cin >> a >> b;

    string sa = to_string(a), sb = to_string(b);
    int la = sa.size(), lb = sb.size();

    long long max_product = -1;
    string ans;
    memset(dp, -1, sizeof dp);
    dp[0][1][1] = 1;
    for (int l = 0; l < N; ++l) {
        for (int f1 = 0; f1 < 3; ++f1) {
            for (int f2 = 0; f2 < 3; ++f2) {
                if (dp[l][f1][f2] == -1)
                    continue;

                for (int d = 0; d <= 9; ++d) {
                    // dp[i][f1][f2] + d -> dp[i + 1][nf1][nf2]
                    int nf1 = get_nf(f1, d, l < la ? sa[l] : '0'), nf2 = get_nf(f2, d, l < lb ? sb[l] : '0');
                    if (dp[l + 1][nf1][nf2] <= dp[l][f1][f2] * d) {
                        dp[l + 1][nf1][nf2] = dp[l][f1][f2] * d;
                        opt[l + 1][nf1][nf2] = opt[l][f1][f2];
                        opt[l + 1][nf1][nf2].push_back('0' + d);
                    }

                    if (is_ok(l, la, lb, f1, f2)) {
                        if (max_product < dp[l][f1][f2]) {
                            max_product = dp[l][f1][f2];
                            ans = opt[l][f1][f2];
                        }
                    }
                }
            }
        }
    }

    cout << ans;
}

Задача (Настя и табло)

На табло с цифрами перестало гореть k “палочек”. По текущей конфигерации табло и числу k необходимо понять, какое максимальное число может гореть на табло, если включить ровно k палочек (из тех которые сейчас выключены)?

Решение

Пусть dp[i][j]=true, если на суффиксе i…n можно включить ровно j палочек и получить корректную последовательность цифр и false иначе. Такую динамику легко пересчитать: будем делать переходы во все возможные цифры, которые являются надмаскми нашей маски на позиции i.

Построение динамики занимает O(10nd).

Теперь пойдём в порядке от 1 до n и будем пробовать жадно ставить максимально возможную цифру, используя нашу динамику. Легко понять, что таким образом мы получим максимально возможное число из n цифр.

Код

1
// code will be here

Бонус

На самом деле здесь мы неявно познакомились с техникой ДП по подмножествам, когда нам прилось как-то кодировать “палочки”. Эта техника очень часто помогает в задачах на ДП, когда необходимо перебрать все подмножества.