一些 dp 建模&优化总结
请别遗忘 / 我用泪与火尽力歌唱
请别遥望 / 林野与花海覆上风霜
一些 DP 建模
染色,距离模型
我们以子树为单位进行建模。把染色视作覆盖距离其 \(\le k\) 的点。
考虑子树里只有两种可能:
- 没有全部覆盖:此时我们只需要关心没有被覆盖的最深的点。
- 全部覆盖:我们需要刻画向外还能覆盖多少个点。
于是我们就有了 dp 状态:设 \(g(u,j)\) 表示 \(u\) 子树内没被完全覆盖,最深距离为 \(j\) 的方案数,\(f(u,j)\) 表示 \(u\) 子树被完全覆盖,还能向外延申 \(j\) 格的方案数。初始状态 \(f(u,k) =g(u,0) = 1\)。分别对应 \(u\) 染色/不染色。
然后考虑 \(u\) 和其子树 \(v\) 的合并,分类讨论:
- \(f(u,j_1)\times f(v,j_2)\to f(u,\max(j_1,j_2-1))\)。
- \(f(u,j_1)\times g(v,j_2)\to f(u,j_1)[j_1\ge j_2+1]\)。
- \(f(u,j_1)\times g(v,j_2)\to g(u,j_2+1)[j_1\lt j_2+1]\)。
- \(g(u,j_1)\times f(v,j_2)\to f(u,j_2-1)[j_1\le j_2-1]\)。
- \(g(u,j_1)\times f(v,j_2)\to g(u,j_1)[j_1\gt j_2-1]\)。
- \(g(u,j_1)\times g(v,j_2)\to g(u,\max(j_1,j_2+1))\)。
这里其实隐含了一个东西,就是为什么我们可以这么设计状态?\(f\to g\) 的转移不会影响到外面的点吗?
其实不会。因为整颗子树,只要最深那个点满足了,其它点也能满足,当然可以用 \(g\) 记。
时间复杂度 \(\mathcal O(nk^2)\)。膜拜伟大 cmd!!!
void dfs(int u, int fa) {
f[u][k] = g[u][0] = 1;
for(auto& v : G[u]) {
if(v == fa)
continue ;
dfs(v, u);
for(int i = 0;i <= k;++ i)
tmpf[i] = tmpg[i] = 0;
for(int p = 0;p <= k;++ p)
for(int q = 0;q <= k;++ q) {// add(u, v) 等价于 (u += v) %= mod
add(tmpf[std::max(p, q - 1)], 1ll * f[u][p] * f[v][q] % mod);
add(tmpg[std::max(p, q + 1)], 1ll * g[u][p] * g[v][q] % mod);
if(p >= q + 1)
add(tmpf[p], 1ll * f[u][p] * g[v][q] % mod);
else
add(tmpg[q + 1], 1ll * f[u][p] * g[v][q] % mod);
if(p <= q - 1)
add(tmpf[q - 1], 1ll * g[u][p] * f[v][q] % mod);
else
add(tmpg[p], 1ll * g[u][p] * f[v][q] % mod);
}
for(int i = 0;i <= k;++ i)
f[u][i] = tmpf[i], g[u][i] = tmpg[i];
}
return ;
}
int main() {
scanf("%d %d", &n, &k);
for(int i = 2;i <= n;++ i) {
int u, v;
scanf("%d %d", &u, &v);
G[u].pb(v);
G[v].pb(u);
}
dfs(1, 0);
int ans = 0;
for(int i = 0;i <= k;++ i)
add(ans, f[1][i]);
printf("%d\n", ans);
return 0;
}
这种 dp 建模的方法精髓在于:
- 关注子树的状态,对子树进行讨论。
- 关注距离相关的特征元素,树上最有这个特征的就是最深的点。
- 向上延申,向下扩展这种思路在 [NOI2020] 命运 一题也有简单体现。
二序列匹配型
就遇到了两道题,以后再遇到别的会继续更新。
总体上大概是,两个序列匹配,求一个最大权值之类,这时候 dp 状态可以设计为 \(f_i\) 表示第一个序列中第 \(i\) 个一定匹配上时候的权值,然后用数据结构优化。
[POI2007] KLO
劳动节的时候写到了这道题,这次遇到类似的模型却想不起来,还是要多加练习。
一个显然的状态设计是,\(f_{i,j}\) 表示前 \(i\) 个删去 \(j\) 个的最大价值。
\(\mathcal O(n^2)\),可以滚动数组到空间 \(\mathcal O(n)\)。这么不牛。
我们给出一类通用的,更加强大的 dp 状态设计:
观察到这个东西实际上可以视作序列 \(a\) 和排列 \(1,2,\dots,n\) 作匹配。
我们只关心能匹配上的点,中间被删除的点占用了我们大量的状态和转移。
几天前的总结里提到,这种东西可以通过 dp 状态的设计转化到转移里,然后用多样的 dp 优化方式 / 数据结构来处理。
这道题里,关键要素是匹配上的点,我们这样设计状态:\(f_i\) 表示 \(a_i\) 能匹配上的最大价值。
转移:\(f_i=\max\limits_j\{(f_j+1)[i\gt j][a_i\gt a_j][a_i-a_j\le i-j] \}\)。
后面两个条件满足的时候,第一个条件显然满足。
然后这个问题转化为一个二维偏序问题,按 \(i-a_i\) 排序后,BIT 维护前缀最大值即可。时间复杂度 \(\mathcal O(n\log n)\)。
#include <bits/stdc++.h>
#define fir first
#define sec second
using pii = std::pair<int, int>;
const int maxn = 1e5 + 5;
const int maxm = 1e6 + 5;
void chkmax(int& x, int y) { if(y > x) x = y; return ; }
int n, cnt = 1e6, c[maxm];
pii a[maxn];
int lowbit(int x) {
return x & -x;
}
void add(int x, int y) {
for(;x <= cnt;x += lowbit(x))
chkmax(c[x], y);
return ;
}
int query(int x) {
int ans = 0;
for(;x;x -= lowbit(x))
chkmax(ans, c[x]);
return ans;
}
int main() {
scanf("%d", &n);
for(int i = 1;i <= n;++ i)
scanf("%d", &a[i].fir), a[i].sec = i;
std::sort(a + 1, a + 1 + n, [&](const pii& lhs, const pii& rhs) {
return (lhs.sec - lhs.fir == rhs.sec - rhs.fir) ? lhs.fir < rhs.fir : lhs.sec - lhs.fir < rhs.sec - rhs.fir;
});
for(int i = 1;i <= n;++ i) {
if(a[i].sec < a[i].fir)
continue ;
int x = query(a[i].fir - 1) + 1;
add(a[i].fir, x);
}
printf("%d\n", query(cnt));
return 0;
}
[CF1334F] Strange Function
对不起,是我太 naive 了 QAQ。
判断有无解直接一个子序列自动机上去就好,以下默认有解。
首先考虑暴力 dp:设 \(f_{i,j}\) 表示前 \(a_{1\sim i}\) 和 \(b_{1\sim j}\) 匹配的最小价值。
然后我不会了啊。但是这种思路是可行的,首先 \(b\) 是单调递增的,那么 \(a_{i+1}\) 每次转移对 \(f_{i+1,j}\) 的影响都是一段区间,直接树状数组维护其差分序列即可 qwq,时间复杂度 \(\mathcal O(n\log n)\),膜拜 cmd!
另一种方法:这还是一个匹配问题啊,所以我们设 \(f_i\) 表示强制匹配上 \(i\) 的最大代价(这里把删除转化为了保留,方便处理),因为 \(b\) 单增,所以能匹配的话 \(b\) 的位置一定是固定的,记和 \(a_i\) 匹配的为 \(b_j\)。
则 \(f_i=\max\limits_{0\le k\lt i,a_k=b_{j-1}}\{f_k+\sum\limits_{x=k+1}^{i-1}[a_t\le a_k][p_t\gt 0]p_t \} + p_i\)。
好难打的 LaTeX。
拆解问题,\(\max\) 里面那个东西和 \(i\) 关系不是太大,拆出来维护。
令 \(g_k\) 为 \(\max\) 里面那一串式子,考虑 \(i\to i+1\) 的时候 \(g\) 数组会怎样变化。
如果 \(p_i\le 0\) 直接略过,反之,对于所有 \(a_i\le a_k\) 的 \(k\),都会产生 \(p_i\) 的贡献。然后 \(g_i=f_i\)。
发现这个东西和值域有关,所以考虑放到值域上维护,就是一个后缀加,单点查询,单点取 \(\max\),值域树状数组可以维护啊,第三个操作转化成前两个操作就好。非常优雅。myh 太强辣!
时间复杂度 \(\mathcal O(n\log n)\)。
基于分段的 dp 转移优化
[JOISC 2020 Day1] 建筑装饰 4
这么难的题为啥 lg 上才蓝题啊,是不是恶评 /fn/fn。
首先有一个 shaber dp:设 \(f(i,j)\) 表示前 \(i\) 个里面选了 \(j\) 个 \(A\) 且第 \(i\) 个选了 \(A_i\),\(g(i,j)\) 表示前 \(i\) 个里面选了 \(j\) 个 \(A\) 且第 \(i\) 个选了 \(B_i\),转移 trivial。
bitset 优化能过 \(n=10^5\),但这是 \(n=5\times 10^5\),\(\mathcal O(\frac{n^2}{\omega})\) 的复杂度基本上冲不过去。但是用 WC2017 挑战 那题里面的卡常数方法也许能过。
然后发现,这个东西信息量小的可怜,这种时候可以打表之类的套取一些信息。
经过打表,发现满足 \(f(i,j)\) 为 \(1\) 的 \(j\) 是连续的一段,然后就可以通过这道题了。
为什么呢?不知道。不会。菜。
[AGC040E] Prefix Suffix Addition
波特智慧题。
考虑只有第一种操作的时候,答案怎么算?其实就是 LDS 的长度。
然后考虑第二种操作的影响是什么?就是往上加了一个递增序列。然后这个的答案就是 LIS 的长度。
那么一个自然的想法是拆开二者,即 \(a_i=p_i+q_i\),然后分别计算 \(p,q\) 的不下降/不上升子段个数。
设 \(f_{i,j}\) 表示 \(a_i=(a_i-j)+j\) 时的最小答案,则 \(f_{i,j}=\min\limits_{k\le a_{i-1}}\{f_{i-1,k}+[k<j]+[a_{i-1}-k>a_i-j] \}\)。
这个转移方程及其特殊,因为不管 \(j\) 有多少,\(k\) 的取值范围都是一样的,然后附加权值 \(\le 2\),所以 \(f_i\) 数组的极差不超过 2。
然后再来考虑这个性质能带给我们什么。注意到对于前 \(i\) 个数,\(j\) 一定是取得越小越好。不难理解,\(p\) 要单增,\(q\) 要单减,这样取显然更能满足条件。
然后我们就能得知,\(f_i\) 一定呈现一个 \(0,0,0,\dots,1,1,1,\dots,2,2,2\) 的形式。那么我们维护两个值 \(f_0,f_1\),代表 \([0,f_0]\) 为 \(0\),\([f_0+1,f_1]\) 为 \(1\),\([f_1+1,a_i]\) 为 \(2\)。转移的时候需要细致地枚举,这个枚举非常困难啊!
好吧,对我来说多少还是有点太超前了,学不会。先放个写得不错得 题解 在这,以后再说。
基于单调性的 dp 转移优化
P6563, AGC007D, [IOI 2018] meetings, 还有 HDU 多校的某场一道 dp 博弈题,忘了是啥。
P6563 [SBCOI 2020] 一直在你身边
一眼 MO 改编题。设 \(f(l,r)\) 表示已经确定 \(x\in [l,r]\),还需要的最小花费,则 \(f(l,r)=\min\limits_{x\in [l,r)} \{a_x+\max(f(l,x),f(x+1,r))\}\)。
\(\mathcal O(n^3)\)。考虑怎么优化。发现外面那层形似单调队列优化 DP,是一个滑动窗口 RMinQ 的形式,但是里面那个 \(\max(f(l,x),f(x+1,r))\) 非常难受,如果固定 \(l,x\),代价会随 \(r\) 增大而变化。
对于这类题目,我们有套路的解决方法:尝试寻找 \(\max\) 函数内的单调性,从而刻画其分段模式。
注意到,如果我们固定 \(l,r\),那么 \(x\) 递增时,\(f(l,x)\) 递增,\(f(x+1,r)\) 递减。
换言之,存在一个决策点 \(p\),满足 \(\forall x\in [l,p),\max(f(l,x),f(x+1,r))=f(x+1,r),\forall x\in [p,r],\max(f(l,x),f(x+1,r))=f(l,x)\)。
这个枚举方式值得注意,我们需要一个高明一点的枚举手法。考虑固定 \(r\),那么 \(l,p\) 单调递减,\(p\) 可以均摊 \(\mathcal O(1)\) 推出来,然后分别用两个单调队列优化。不过这题 \(a\) 递增,所以右边有更强的单调性,直接取 \(p\) 即可,省一个单调队列。
时间复杂度 \(\mathcal O(n^2)\)。
#include <bits/stdc++.h>
using i64 = long long;
int read() {
int s = 0;
char c = getchar();
bool f = true;
for(;c < '0'||c > '9';c = getchar())
if(c == '-')
f = false;
for(;c >= '0'&&c <= '9';c = getchar())
s = (s << 1) + (s << 3) + (c ^ '0');
return f ? s : -s;
}
const int maxn = 7105;
int n, a[maxn], Q[maxn], head, tail;
i64 f[maxn][maxn];
void chkmin(i64& x, i64 y) {
if(y < x)
x = y;
return ;
}
void work() {
n = read();
for(int r = 1;r <= n;++ r) {
a[r] = read();
int p = r - 1;
head = 1, tail = 0;
for(int l = r - 1;l >= 1;-- l) {
while(p > l&&f[l][p - 1] > f[p][r])
-- p;
f[l][r] = f[l][p] + a[p];
while(head <= tail&&Q[head] >= p)
++ head;
while(head <= tail&&a[Q[tail]] + f[Q[tail] + 1][r] >= a[l] + f[l + 1][r])
-- tail;
Q[++ tail] = l;
chkmin(f[l][r], f[Q[head] + 1][r] + a[Q[head]]);
}
}
printf("%lld\n", f[1][n]);
return ;
}
int main() {
int T = read();
while(T --)
work();
return 0;
}
这种基于单调性分析得出分段性质从而去掉 \(\max/\min\) 函数的手法还有例题。来看一个简单练习题。
[AGC007D] Shik and Game
坐标这么大,还是可以使用我们在 [UR #13 B] Ernd 中提到的,状态里只记元素编号,坐标什么的放到转移里分段考虑。
设 \(f_i\) 表示前 \(i\) 个金币拿到的最小时间花费,那么可以列出:\(f_i=\min\limits_{j\lt i} \{f_j+x_i-x_j+\max(2(x_i-x_{j+1}), T) \}\)。
这个形式非常丑陋啊,写得好看一点,令 \(g_i = f_i - x_i\),则 \(g_i=\min\limits_{j<i}\{g_j+\max(2(x_i-x_{j+1}), T) \}\)。
一样的处理手法,固定 \(i\),当 \(j\) 递减,\(2(x_i-x_{j+1})\) 是递增的,那么又存在一个分段点 \(p\),然后又可以快乐单调队列了。
但是我们的代码还可以更简短!这道题核心代码我只需要 4 行!
注意到 \(p\) 单增,我们求得是 前缀最大值 而不是 区间最大值,所以 \(p\) 左边的单调队列可以省掉。然后 \(f\) 显然单增,那么右边那个式子也可以直接取 \(p\)。时间复杂度 \(\mathcal O(n)\),代码不超过 400B。贴个主函数。
int main() {
scanf("%d %lld %lld", &n, &E, &T);
int p = 0;
i64 r = 1e18;
for(int i = 1;i <= n;++ i) {
scanf("%lld", &x[i]);
while(p < i&&(x[i] - x[p + 1]) * 2 >= T)
chkmin(r, f[p] - 2 * x[p + 1]), ++ p;
chkmin(f[i] = r + 2 * x[i], f[p] + T);
}
printf("%lld\n", E + f[n]);
return 0;
}
[IOI 2018] meetings
这题的其它细节之前发过一次,就不写了。
考虑固定 \(l\),然后算出 \(f(l,l\sim r)\),因为涉及区间最大值,我们考虑笛卡尔树上分治。设 \(x\) 为 \([l,r]\) 间最大值,那么:
我们现在只考虑 \([l,x-1]\) 和 \([x+1,r]\) 的合并,这两部分单独可以分治下去解决。
套用本文的方法,我们发现,固定 \(l\),让 \(r\) 递增时,左右都是一个单增的结构。没法优化了吗?
我们需要更细致的分析。因为是离散的,我们做一个差分,不难发现 \(f(x+1,r+1)-f(x+1,r)\le h_x\),这是 RHS 的差分,而 LHS 的差分恒为 \(h_x\),也就是说,存在一个分界点 \(p\),在 \(p\) 左侧 \(\rm LHS\le RHS\),而右侧 \(\rm LHS\ge RHS\)。
因为 LHS 是一个一次函数的形式啊,我们要在线段树上二分,然后打 2 个不同的 tag,挺难受的其实,当时写的很痛苦,现在也很不想再写一遍。
从这道题可以学到,两侧同增 / 同减,可以做一些求导(差分)之类的细致分析求解。