一些 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\) 个的最大价值。

\[f(i,j)=\max\{f(i-1,j)+[a_i=i-j],f(i-1,j-1)\} \]

\(\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]\) 间最大值,那么:

\[f(l,r) = \min\{f(l,x-1)+(r-x+1)\times h_x, f(x+1,r) + (x -l + 1)\times h_x \} \]

我们现在只考虑 \([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,挺难受的其实,当时写的很痛苦,现在也很不想再写一遍。

从这道题可以学到,两侧同增 / 同减,可以做一些求导(差分)之类的细致分析求解。

posted @ 2024-02-22 23:57  ImALAS  阅读(32)  评论(0编辑  收藏  举报