DP 杂题
Trick
-
优化 dp 状态找题目的特殊性质。(XXYX Binary Tree)
-
对于有多组修改,但只是独立修改一个位置权值的 dp 题,可以考虑去讨论是否选择被修改的这个位置。(Contest with Drinks Hard)
-
对于不好优化的序列 dp,可以考虑放在分治上做。(Yet Another Partiton Problem)
-
对于 \(\min(B_j+C_k-i,A_i+C_k-j,A_i+B_j-k)\) 这样的形式,可以变成 \(A_i+B_j+C_k-\max(i+A_i,j+B_j,k+C_k)\) 进行求解。(赌局)
题目
[ARC157E] XXYX Binary Tree
一个很显然的暴力,\(f_{u,a,b,c}\) 表示在在 \(u\) 的子树中,是否有 \(a\) 个 XX
,\(b\) 个 XY
,\(c\) 个 YX
。
这个状态 \(O(n^4)\) 的,考虑优化,可以先省去一个 \(c\),变成 \(f_{u,a,b}\),因为 \(a+b+c\) 的总和是知道的。
然后优化不了了……
只能重新设计,发现都没有用上二叉树的条件,显然要找一点性质的。
可以发现 \(Y\) 的个数好像可以确定,如果 \(Y\) 为非根节点,那么 \(Y\) 的数量有 \(B\) 个,否则就有 \(B+1\) 个,因为一个除了根节点的 \(Y\) 肯定可以贡献一个 XY
。
还可以发现非叶子的 \(Y\) 可以贡献两个 YX
。
然后就可以刻画 XY
和 YX
的数量了,这里分根节点是否为 \(Y\) 两种情况。
如果根不为 \(Y\),那么一共有 \(B\) 个 \(Y\),记叶子节点为 \(Y\) 的个数为 \(sum\),那么 YX
的数量就是 \(2(B-sum)\),所以 \(sum\) 满足 \(sum=B-\frac{C}{2}\)。
如果根为 \(Y\),那么一共有 \(B+1\) 个 \(Y\),记叶子节点为 \(Y\) 的个数为 \(sum\),那么 YX
的数量就是 \(2(B+1-sum)\),所以 \(sum\) 满足 \(sum=B+1-\frac{C}{2}\)。
所以设 \(f_{u,i,0/1}\) 表示 \(u\) 的子树中有 \(i\) 个叶子为 \(Y\) 且 \(u\) 是否是 \(Y\) 时最多有多少个非叶子的 \(Y\)。
这里可以 \(O(n^2)\) 树上背包转移。
代码
#include <bits/stdc++.h>
void Freopen() {
freopen("", "r", stdin);
freopen("", "w", stdout);
}
using namespace std;
const int N = 1e4 + 10, M = 2e5 + 10, inf = 1e9, mod = 998244353;
int n, A, B, C;
int f[N][N][2], siz[N];
vector< int> G[N];
void dfs( int u) {
siz[u] = 0;
if (! G[u].size()) {
siz[u] = 1;
f[u][0][0] = 0, f[u][1][1] = 0;
return ;
}
f[u][0][0] = 0, f[u][0][1] = 1;
for ( auto v : G[u]) {
dfs(v);
// 这里注意,必须要倒叙枚举,这样才能保证不会用到已经被转移过的信息
for ( int i = siz[u]; i >= 0; i --)
for ( int j = siz[v]; j >= 0; j --) {
f[u][i + j][0] = max(f[u][i + j][0], f[u][i][0] + max(f[v][j][0], f[v][j][1]));
f[u][i + j][1] = max(f[u][i + j][1], f[u][i][1] + f[v][j][0]);
}
siz[u] += siz[v];
}
}
void solve() {
cin >> n >> A >> B >> C;
for ( int i = 1; i <= n; i ++) {
G[i].clear();
for ( int j = 0; j <= B + 1; j ++)
f[i][j][0] = f[i][j][1] = -inf;
}
for ( int i = 2, fa; i <= n; i ++)
cin >> fa, G[fa].push_back(i);
if (C & 1) return cout << "No\n", void();
dfs(1);
if (B - C / 2 >= 0 && f[1][B - C / 2][0] >= C / 2) {
cout << "Yes\n";
return ;
}
if (B + 1 - C / 2 >= 0 && f[1][B + 1 - C / 2][1] >= C / 2) {
cout << "Yes\n";
return ;
}
cout << "No\n";
}
signed main() {
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int t; cin >> t;
while (t --) solve();
return 0;
}
[ARC066F] Contest with Drinks Hard
它这个贡献形式就已经表明了这是一个 1d 问题。
设 \(f_i\) 表示考虑了前 \(i\) 题的最大值。
转移就是枚举 \(j\),\((j+1,i]\) 的题目都要做。
当然也可以不做第 \(i\) 题,直接从 \(f_{i-1}\) 转移,记 \(cost(l,r)=\dfrac{(r-l+1)(r-l+2)}{2}-sum_r+sum_{l-1}\)。
那么 \(f_i\) 转移就有:
把 \(cost(j+1,i)\) 拆开,可以直接斜率优化。
但是这个题麻烦的是有多组修改,直接做 \(O(nm)\) 过不了。
但特殊在于,每个修改都是独立的,所以考虑讨论是否选择被修改的那道题。
记 \(f_i\) 表示考虑了前 \(i\) 个题的最大值;\(g_i\) 表示考虑了后 \(i\) 个题的最大值;\(h_i\) 表示考虑了所有题,但是钦定第 \(i\) 题必做的最大值。
假设题目 \(i\) 被修改后,考虑是否选择它,如果不选择 \(i\),那么答案就是 \(f_{i-1}+g_{i+1}\);如果选择,答案就是 \(h_i-x+a_i\)。
所以最后的答案就是就是 \(\max(f_{i-1}+g_{i+1},h_i-x+a_i)\)。
\(f_i\) 转移和上述一样,斜率优化即可;\(g_i\) 也就是倒序 dp,和 \(f_i\) 相似。
考虑 \(h_i\) 怎么求?直接转移就是:
这里转移是 \(O(n^3)\) 的。
因为 \(l,r\) 要跨过 \(i\),那么不妨考虑分治。
设 \(dp_i\) 左端点为 \(i\) 且 \(i\in[l,mid]\),右端点为 \(j-1\) 且 \(j\in[mid+1,r+1]\) 时,\(f_{i-1}+g_{j}+cost(i,j-1)\) 的最大值。
这里也是可以斜率优化去求的,求出来后 \(dp_i\) 可以给 \(h_{i\sim mid}\) 贡献最大值。
当然,因为 \(dp_i\) 只能给 \(h_{i\sim mid}\) 贡献最大值,那么再设一个和 \(dp_i\) 相反的能够给 \(h_{mid+1\sim i}\) 贡献最大值的 \(dp'_i\) 即可。
实现这块,我用的是李超线段树,多带一个 \(\log\) 但是无伤大雅。
代码
#include <bits/stdc++.h>
#define int long long
void Freopen() {
freopen("", "r", stdin);
freopen("", "w", stdout);
}
using namespace std;
const int N = 3e5 + 10, M = 2e5 + 10, inf = 1e18, mod = 998244353;
int n, m;
int a[N], sum[N];
int f[N], g[N], h[N];
int tot, rt;
struct sgt {
#define mid ((l + r) >> 1)
int tag[N * 18], ls[N * 18], rs[N * 18];
struct line {
int k, b;
} l[N];
int cal( int id, int x) {
return l[id].k * x + l[id].b;
}
void insert( int u, int & k, int l = 1, int r = n) {
if (! k) return k = ++ tot, ls[k] = 0, rs[k] = 0, tag[k] = u, void();
int & v = tag[k];
if (cal(v, mid) < cal(u, mid)) swap(v, u);
if (cal(u, l) > cal(v, l)) insert(u, ls[k], l, mid);
if (cal(u, r) > cal(v, r)) insert(u, rs[k], mid + 1, r);
}
int ask( int x, int k, int l = 1, int r = n) {
if (! k) return -inf;
return max(cal(tag[k], x), x <= mid ? ask(x, ls[k], l, mid) : ask(x, rs[k], mid + 1, r));
}
#undef mid
} tr;
void solve( int l, int r) {
if (l == r) {
h[l] = max(h[l], 1 - a[l]);
return ;
}
int mid = (l + r) >> 1;
solve(l, mid), solve(mid + 1, r);
rt = tot = 0;
for ( int i = mid + 1; i <= r + 1; i ++) {
tr.l[i] = {-i, g[i] - sum[i - 1] + (i * i + i) / 2};
tr.insert(i, rt);
}
int mx = -inf;
for ( int i = l; i <= mid; i ++) {
mx = max(mx, f[i - 1] + tr.ask(i, rt) + (i * i - i) / 2 + sum[i - 1]);
h[i] = max(h[i], mx);
}
rt = tot = 0;
for ( int i = mid; i >= l - 1; i --) {
tr.l[i] = {-i, f[i] + sum[i] + (i * i - i) / 2};
tr.insert(i, rt);
}
mx = -inf;
for ( int i = r; i > mid; i --) {
mx = max(mx, g[i + 1] + tr.ask(i, rt) + (i * i + i) / 2 - sum[i]);
h[i] = max(h[i], mx);
}
}
signed main() {
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n;
for ( int i = 1; i <= n; i ++) cin >> a[i], sum[i] = sum[i - 1] + a[i];
memset(h, 128, sizeof h);
rt = tot = 0;
tr.l[0] = {0, 0};
tr.insert(0, rt);
for ( int i = 1; i <= n; i ++) {
f[i] = f[i - 1];
f[i] = max(f[i], tr.ask(i, rt) + (i * i + i) / 2 - sum[i]);
tr.l[i] = {-i, f[i] + sum[i] + (i * i - i) / 2};
tr.insert(i, rt);
}
rt = tot = 0;
tr.l[n + 1] = {-(n + 1), -sum[n] + ((n + 1) * (n + 1) + n + 1) / 2};
tr.insert(n + 1, rt);
for ( int i = n; i; i --) {
g[i] = g[i + 1];
g[i] = max(g[i], tr.ask(i, rt) + (i * i - i) / 2 + sum[i - 1]);
tr.l[i] = {-i, g[i] - sum[i - 1] + (i * i + i) / 2};
tr.insert(i, rt);
}
solve(1, n);
cin >> m;
while (m --) {
int i, x; cin >> i >> x;
cout << max(f[i - 1] + g[i + 1], h[i] - x + a[i]) << '\n';
}
return 0;
}
Yet Another Partiton Problem
2d 问题。
暴力的 dp 是简单的,设 \(f_{i,l}\) 表示考虑到第 \(i\) 个数,已经分了 \(l\) 段的最小代价。
转移是 \(O(n^2k)\) 的,所以要优化转移的复杂度。
这个东西,显然是没有决策单调性的,数据结构啥的也优化不了,主要是最大值的贡献太难表示了。
遇到不会优化的序列题就分治一下,考虑 \([l,mid]\) 转移到 \((mid,r]\)。
这里设 \(g_i\) 为上一轮的 dp 值,\(f_i\) 为此轮的。
记 \(mx1_i\) 为 \((i,mid]\) 的最大值,\(mx2_i\) 为 \((mid,i]\) 的最大值。
那么转移有 \(f_i\leftarrow g_j+(i-j)\times\max(mx1_j,mx2_i)\)。
这里显然可以讨论 \(mx1_j\) 和 \(mx2_i\) 的大小。
如果 \(mx1_j\le mx2_i\),那么就有 \(f_i\leftarrow g_j-j\times mx2_i+i\times mx2_i\)。
这个东西可以直接上李超树去做,因为 \(mx1_j\) 与 \(mx2_i\) 本来就有序,就可以像 CDQ 那样搞:对于 \(mx1_j \le mx2_i\) 的 \(j\),把 \(y=-jx+g_j\) 这条直线插入李超,询问 \(x=mx2_i\) 处的最小值即可。
如果 \(mx1_j\gt mx2_i\),做法和上述同理。
代码
#include <bits/stdc++.h>
void Freopen() {
freopen("", "r", stdin);
freopen("", "w", stdout);
}
using namespace std;
const int N = 2e5 + 10, M = 2e5 + 10, inf = 1e9, mod = 998244353;
int Mx;
int n, k;
int a[N];
int f[N], g[N];
struct line {
int k, b;
} l[N];
int cal( int id, int x) {
return l[id].k * x + l[id].b;
}
int tag[N * 10], ls[N * 10], rs[N * 10];
int rt, tot;
#define mid ((l + r) >> 1)
void insert( int & k, int l, int r, int u) {
if (! k) return k = ++ tot, ls[k] = rs[k] = 0, tag[k] = u, void();
int & v = tag[k];
if (cal(v, mid) > cal(u, mid)) swap(v, u);
if (cal(u, l) < cal(v, l)) insert(ls[k], l, mid, u);
if (cal(u, r) < cal(v, r)) insert(rs[k], mid + 1, r, u);
}
int ask( int k, int l, int r, int x) {
if (! k) return inf;
return min(cal(tag[k], x), x <= mid ? ask(ls[k], l, mid, x) : ask(rs[k], mid + 1, r, x));
}
#undef mid
int mx1[N], mx2[N];
void CDQ( int l, int r) {
if (l == r) return ;
int mid = (l + r) >> 1;
CDQ(l, mid), CDQ(mid + 1, r);
rt = tot = 0;
mx1[mid] = mx2[mid] = 0;
for ( int i = mid - 1; i >= l; i --)
mx1[i] = max(mx1[i + 1], a[i + 1]);
for ( int i = mid + 1; i <= r; i ++)
mx2[i] = max(mx2[i - 1], a[i]);
int j = mid;
for ( int i = mid + 1; i <= r; i ++) {
while (mx1[j] <= mx2[i] && j >= l) {
:: l[j] = {-j, g[j]};
insert(rt, 1, Mx, j);
j --;
}
f[i] = min(f[i], ask(rt, 1, Mx, mx2[i]) + i * mx2[i]);
}
rt = tot = 0;
j = l;
for ( int i = r; i > mid; i --) {
while (mx1[j] > mx2[i] && j <= mid) {
:: l[j] = {mx1[j], g[j] - j * mx1[j]};
insert(rt, 1, n, j);
j ++;
}
f[i] = min(f[i], ask(rt, 1, n, i));
}
}
signed main() {
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> k;
for ( int i = 1; i <= n; i ++) cin >> a[i];
for ( int i = 1; i <= n; i ++)
Mx = max(Mx, a[i]), f[i] = Mx * i;
k -= 1;
while (k --) {
for ( int i = 1; i <= n; i ++) g[i] = f[i];
for ( int i = 1; i <= n; i ++) f[i] = inf;
CDQ(1, n);
}
cout << f[n] << '\n';
return 0;
}
赌局
没有原题。
可以考虑背包去做。
设 \(A_i\) 表示 \(A\) 赢的时候亏损 \(i\) 元时,\(A\) 输了可以获得的最大价值,\(B_i\)、\(C_i\) 同理。
转移是 \(O(n^2V)\) 的。
那么答案就是 \(\min(B_j+C_k-i,A_i+C_k-j,A_i+B_j-k)\)。
发现求答案的复杂度是 \(O(n^3V^3)\) 的,有点炸。
考虑把答案形式化一下,变成 \(A_i+B_j+C_k-\max(i+A_i,j+B_j,k+C_k)\),这就很典了。
考虑钦定 \(\max(i+A_i,j+B_j,k+C_k)\) 是哪一个,然后用树状数组维护一个前缀最大值即可。
复杂度 \(O(n^2V+nV\log{nV})\)。
代码
#include <bits/stdc++.h>
void Freopen() {
freopen("", "r", stdin);
freopen("", "w", stdout);
}
using namespace std;
const int N = 5e5 + 10, M = 2e5 + 10, inf = 1e9, mod = 998244353;
int n;
int A[N], B[N], C[N];
struct bit {
int tr[N];
void init() {
for ( int u = 1; u < N; u ++) tr[u] = -inf;
}
void add( int u, int v) {
for (; u < N; u += (u & -u)) tr[u] = max(tr[u], v);
}
int ask( int u, int res = -inf) {
for (; u; u -= (u & -u)) res = max(res, tr[u]);
return res;
}
} ta, tb, tc;
signed main() {
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
memset(A, 128, sizeof A);
memset(B, 128, sizeof B);
memset(C, 128, sizeof C);
A[0] = B[0] = C[0] = 0;
ta.init(), tb.init(), tc.init();
cin >> n;
for ( int i = 1; i <= n; i ++) {
int t, a, b; cin >> t >> a >> b;
for ( int j = n * 500; j >= 0; j --)
if (j - a >= 0) {
if (t == 1) A[j] = max(A[j], A[j - a] + b);
else if (t == 2) B[j] = max(B[j], B[j - a] + b);
else C[j] = max(C[j], C[j - a] + b);
}
}
int ans = 0;
for ( int i = 0; i <= n * 500; i ++) {
if (A[i] > -inf) ta.add(i + A[i] + 1, A[i]);
if (B[i] > -inf) tb.add(i + B[i] + 1, B[i]);
if (C[i] > -inf) tc.add(i + C[i] + 1, C[i]);
}
for ( int i = 0; i <= n * 500; i ++) {
if (A[i] > -inf) ans = max(ans, tb.ask(i + A[i] + 1) + tc.ask(i + A[i] + 1) - i);
if (B[i] > -inf) ans = max(ans, ta.ask(i + B[i] + 1) + tc.ask(i + B[i] + 1) - i);
if (C[i] > -inf) ans = max(ans, ta.ask(i + C[i] + 1) + tb.ask(i + C[i] + 1) - i);
}
cout << ans << '\n';
return 0;
}