ДП по подмножествам и цифрам. Разбор задач
Оглавление
Запись занятия
Задача (скука)
Задана последовательность 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
Бонус
На самом деле здесь мы неявно познакомились с техникой ДП по подмножествам, когда нам прилось как-то кодировать “палочки”. Эта техника очень часто помогает в задачах на ДП, когда необходимо перебрать все подмножества.