NamomoCamp-dp 部分题解


NamomoCamp2020-div2-day1-dp 部分题解

New Year and Original Order (Good Bye 2017 G)

Tag

数位dp

Solution

用$f[d][i]$表示数字$d$在第$i$位上出现的次数,假设我们已知所有$f[d][i]$,则有

可惜的是$f[d][i]$未知,直接求$f[d][i]$也很难求

考虑下性质:所有数字升序排序,转化一下求$f[d][i]$的思路,用$g[d][i]$表示$\ge d$的数字出现$\ge i$次的次数,那么有$f[d][i]=g[d][i]-g[d+1][i]$

$g[d][i]$依旧不好直接求,但可以先求$h[d][i]$:$\ge d$的数字出现恰好$i$次的次数,$g[d][i]=\sum_{j=i}^{n}h[d][j]$

$h$就可以直接求了,数位dp一下即可,状态$h[pos][d][i][limit]$,状态数$O(10N^2)$

$k$表示当前位枚举的数字,转移式

Code
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
69
70
71
72
73
74
75
76
#include <bits/stdc++.h>
using namespace std;

typedef pair<int, int> pi;
typedef long long ll;

const int maxn = 705;
const int inf = 0x3f3f3f3f;
const int mod = 1e9 + 7;

char s[maxn];
int a[maxn];
int n;

int f[10][maxn], w[maxn];
int g[10][maxn];
int h[maxn][10][maxn][2];

int dp(int pos, int d, int i, bool limit) {
if (pos == n + 1) return i == 0 ? 1 : 0;
if (h[pos][d][i][limit] != -1) return h[pos][d][i][limit];
int up = limit ? a[pos] : 9;
int ans = 0;
for (int k = 0; k <= up; k++) {
if (k >= d && i > 0) {
ans += dp(pos + 1, d, i - 1, limit && k == up);
ans %= mod;
} else if (k < d) {
ans += dp(pos + 1, d, i, limit && k == up);
ans %= mod;
}
}
return h[pos][d][i][limit] = ans;
}

int main() {

int T = 1;
// scanf("%d", &T);
while (T--) {
scanf("%s", s + 1);
n = strlen(s + 1);
for (int i = 1; i <= n; i++) {
a[i] = s[i] - '0';
}
memset(h, -1, sizeof(h));
for (int d = 0; d < 10; d++) {
for (int i = 1; i <= n; i++) {
for (int j = i; j <= n; j++) {
g[d][i] += dp(1, d, j, true);
g[d][i] %= mod;
}
}
}
for (int i = 1; i <= n; i++) {
for (int d = 0; d < 9; d++) {
f[d][i] = (g[d][i] - g[d + 1][i] + mod) % mod;
}
f[9][i] = g[9][i];
}
w[0] = 1;
for (int i = 1; i < n; i++) {
w[i] = w[i - 1] * 10ll % mod;
}
int ans = 0;
for (int d = 0; d < 10; d++) {
for (int i = 1; i <= n; i++) {
ans += 1ll * d * f[d][i] * w[i - 1] % mod;
ans %= mod;
}
}
printf("%d\n", ans);
}

return 0;
}


Financiers Game (CF 729 F)

Tag

dp,状态优化,记忆化搜索

Solution

先来想一个暴力的状态,$f[i][j][k][m]$表示左边取$i$个,右边​取$j$个,上一步取了$k$,$m=0/1$表示轮到左/右边取,$f[i][j][k][m]$则表示该状态下的答案

转移显然,状态也记全了,但是这个空间是$N^3$的,开不下。

这题的要点在于注意到$k$的优化,来考虑一下能最大化$k$的情况,肯定是从$2$开始,每次都取$k+1$,即取${2,3,4,..,k}$,此时有$\sum_{i=2}^{k}{i}=\frac{(k+2)\times(k-1)}{2} \leq N$,化简一下,可以得到一个大约的范围$k\leq \sqrt{2N} $

然后再以类似的想法来考虑$j$的优化,首先把$j$从右边取j个变成左右已取的个数之差,考虑最大化$j$,则应该是左边每次取$k+1$,而右边每次取$k$,那么某一时刻,可以表示为 左边取 ${1,2,3,…,k-1,k} $,右边取 ${1,2,3,…,k-1} $,类似上面算一下,可得 $k\leq \sqrt{N} $,因此 $-\sqrt{N}\leq j\leq \sqrt{N} $

优化$j$和$k$后空间其实已经够了,本题内存限制$512MB$,这样大概是$300MB$

但其实$i$也可以优化,最大化$i$的情况与最大化$j$的情况类似,结果大约是$i\leq \frac{N+\sqrt{N}}{2} $,这样就可以跑过$256MB$限制的情况了,过程自己算吧

Code
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
69
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
typedef pair<int, int> pi;
typedef pair<ll, ll> pll;

const int maxn = 100005;
const int inf = 0x3f3f3f3f;
const ll INF = 0x3f3f3f3f3f3f3f3f;
const int mod = 1e9 + 7;

int a[4005];
int pre[4005], suf[4005];
int n;

int f[2100][131][91][2];
const int M = 65;

int dp(int i, int j, int k, int mode) {
int B = i + M - j;
if (mode == 0) {
if (f[i][j][k][mode] != -inf) return f[i][j][k][mode];
if (i + B + k <= n) {
f[i][j][k][0] = max(f[i][j][k][0], dp(i + k, j + k, k, 1) + pre[i + k] - pre[i]);
}
if (i + B + k + 1 <= n) {
f[i][j][k][0] = max(f[i][j][k][0], dp(i + k + 1, j + k + 1, k + 1, 1) + pre[i + k + 1] - pre[i]);
}
if (i + B + k > n) {
f[i][j][k][0] = 0;
}
} else {
if (f[i][j][k][mode] != inf) return f[i][j][k][mode];
if (i + B + k <= n) {
f[i][j][k][1] = min(f[i][j][k][1], dp(i, j - k, k, 0) - suf[n - B - k + 1] + suf[n - B + 1]);
}
if (i + B + k + 1 <= n) {
f[i][j][k][1] = min(f[i][j][k][1], dp(i, j - k - 1, k + 1, 0) - suf[n - B - k] + suf[n - B + 1]);
}
if (i + B + k > n) {
f[i][j][k][1] = 0;
}
}
return f[i][j][k][mode];
}

int main() {

scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d", a + i);
}
pre[0] = 0; suf[n + 1] = 0;
for (int i = 1; i <= n; i++) pre[i] = pre[i - 1] + a[i];
for (int i = n; i >= 1; i--) suf[i] = suf[i + 1] + a[i];

for (int i = 0; i < 2100; i++) {
for (int j = 0; j < 131; j++) {
for (int k = 0; k < 91; k++) {
f[i][j][k][0] = -inf;
f[i][j][k][1] = inf;
}
}
}
printf("%d\n", dp(0, M, 1, 0));

return 0;
}


[ZJOI2012]波浪

Tag
Solution
Code


Game on Chessboard (CCPC 2019 秦皇岛 G)

Tag
Solution
Code


Fountains (ICPC 2020 上海 F)

Tag
Solution
Code


Sum (VK Cup 2019 Final D)

Tag

背包,分治

Solution

首先来看题目给出的特殊性质,每个数组单独来讲都是非递减的。

因此有一个结论,在最优方案中,不存在两个数组同时未取完的情况。使用反证法,如果存在两个数组同时未取完,这两个数组最后取的数分别为$x$和$y$,则若$x>y$,可以把$y$换成$x$右边的数,$x<y$则可以把$x$换成$y$右边的数。

换句话说,最优方案可以视作取完若干个数组,再外加一个取到一半的数组。

那么就诞生了一个暴力做法,枚举那个取到一半的数组,其余$N-1$个数组相当于$N-1$个物品,做一下背包,然后再枚举一下多余的一个数组里取了几个数,和背包结合一下出答案。

可是我们没有办法高效地从一个$N$个物品的背包得到删去里面某一个物品后剩余物品组成的背包。如果暴力做$N$次背包的话。$O(N\times N\times K)$的时间复杂度是不合格的。

然后就是这道题的神奇分治优化。

优化思路是这样的,我们将去除物品$i$的影响后剩下的背包称作$f[i]$,可以发现,$f[\frac{N}{2}+1]…f[N]$有个共同点,那就是都保留了物品$i\in [1, \frac{N}{2}]$对背包的影响,所以可以先求出物品$i\in [1, \frac{N}{2}]$组成的背包,然后再由该背包去推出$f[\frac{N}{2}+1]…f[N]$。

将上面的优化思路推广,想象成一颗线段树的形式,线段树上的某一个结点$x[l,r]$表示的就是去除物品$i\in [l,r]$的影响后的背包。递归下去时,往左儿子的背包中加入右儿子区间内的所有物品的影响,往右儿子的背包中加入左儿子区间内的所有物品的影响。递归到最后$l==r$的叶子结点时,就是我们需要的$f[i]$。

空间上,因为是类似线段树的形式,总共$4\times N$个结点,每个节点一个大小为$K$的背包,空间$O(4\times N\times K)$。

时间上,线段树总共$logN$层,在每一层中,每个物品都会跟其兄弟结点中的背包发生一次合并,因此每一层都是$N$次合并,每次合并$O(K)$,因此时间复杂度$O(logN\times N\times K)$。

Code
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include <bits/stdc++.h>
using namespace std;

typedef pair<int, int> pi;
typedef long long ll;

const int maxn = 3005;
const int inf = 0x3f3f3f3f;
const int mod = 1e9 + 7;

vector<int> a[maxn];
int t[maxn];
ll sum[maxn];

int n, k;

struct Info {
ll f[maxn];
void copy(Info tmp) {
for (int i = 0; i <= k; i++) {
f[i] = tmp.f[i];
}
}
void dp(ll v, int c) {
for (int i = k; i >= c; i--) {
f[i] = max(f[i], f[i - c] + v);
}
}
} info[maxn << 2];

ll pre[1000005];
ll ans;

ll calc(vector<int> &v, int vn, ll f[]) {
ll ret = 0;
for (int i = 1; i <= vn; i++) {
pre[i] = pre[i - 1] + v[i];
}
for (int i = 0; i <= min(vn, k); i++) {
ret = max(ret, f[k - i] + pre[i]);
}
return ret;
}

void solve(int x, int l, int r) {
if (l == r) {
ans = max(ans, calc(a[l], t[l], info[x].f));
} else {
int mid = l + r >> 1;
int ls = x * 2, rs = ls + 1;
info[ls].copy(info[x]);
info[rs].copy(info[x]);
for (int i = mid + 1; i <= r; i++) {
info[ls].dp(sum[i], t[i]);
}
for (int i = l; i <= mid; i++) {
info[rs].dp(sum[i], t[i]);
}
solve(ls, l, mid);
solve(rs, mid + 1, r);
}
}

void run() {
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; i++) {
scanf("%d", t + i);
a[i].push_back(0);
for (int j = 1; j <= t[i]; j++) {
int x; scanf("%d", &x);
a[i].push_back(x);
sum[i] += x;
}
}
solve(1, 1, n);
printf("%lld\n", ans);
}

int main() {

int T = 1;
// scanf("%d", &T);
while (T--) {
run();
}

return 0;
}


Lucky Numbers (CF 1428 G)

Tag
Solution
Code


String Transformation (CF 1383 C)

Tag
Solution
Code


Cells Blocking (300iq contest)

Tag
Solution
Code