题解:P11516 [CCO 2024] Summer Driving

题意

给定一棵 \(n\) 个点的树,有一个棋子初始时在 \(r\) 节点上。Alice 和 Bob 会轮流对棋子进行操作,Alice 先手。

  • Alice 操作时,必须把棋子沿着 \(A\) 条两人都未走过的边移动。
  • Bob 操作时,必须把棋子沿着至多 \(B\) 条边移动。
  • 当 Alice 无法操作时,Bob 必须把棋子沿着至多 \(B\) 条两人都未走过的边移动。游戏结束。

Alice 希望最大化最终棋子所在的节点编号,Bob 则希望最小化最终棋子所在的节点编号。求当两人都以最佳策略行动时,最终棋子所在的节点编号。\(1\leq n\leq 3\times 10^5\)

题解

非常牛的题啊,被放到了 NOIP 模拟赛 T4。

先判掉 \(A\leq B\) 的情况。此时每轮操作 Bob 都可以往节点 \(1\)\(B-A\) 步,因此答案显然可以取到 \(1\)

接下来我们有 \(A>B\)

很容易想到的一步是,我们二分答案 \(mid\),把树上编号 \(\geq mid\) 的点设为 \(1\)\(<mid\) 的点设为 \(0\),那么 check 就是要判断 Alice 是否能走到 \(1\) 点。

\(r\) 视作整棵树的根,当 Alice 从点 \(x\) 开始操作时,不难发现 \(r\)\(x\) 的路径上的边都已经被走过了,因此 Alice 只会走向 \(x\) 子树内的点。而由于 \(B<A\),Bob 操作完后也不会走出 \(sub_x\)。据此考虑树形 DP。令 \(f_u\) 表示 Alice 从点 \(u\) 开始操作,会走到什么点,\(g_u\) 表示 Bob 从点 \(u\) 开始操作,会走到什么点。

考虑 \(f\) 的转移,如果 \(u\) 存在 \(A\) 级儿子,容易列出转移方程:

\[f_u=\bigvee_{\substack{v\in sub_u\\ \operatorname{dist}(u,v)=A}}g_v \]

我们可以预处理出 \(u\) 的所有 \(A\) 级儿子进行转移。具体来说,我们在 DFS 预处理的过程中存储 DFS 栈,那么 \(stk_{top-A}\) 就是 \(stk_{top}\)\(A\) 级祖先。由于每个 \(v\) 恰好会被其 \(A\) 级祖先转移一次,这部分复杂度是 \(\mathcal{O}(n)\) 的。

但是 \(u\) 也可能不存在 \(A\) 级儿子,对应着此时 Alice 无法操作,游戏进入最终阶段。那么我们就需要知道 \(sub_u\) 内是否有与 \(u\) 距离 \(\leq B\) 的点 \(v\),使得 \(v<mid\)。这是简单的,我们树形 DP 出 \(mn_u\) 表示 \(sub_u\) 内满足 \(v<mid\) 的点 \(v\) 的最浅深度,判断是否有 \(mn_u-dep_u>B\) 即可。\(mn\) 的转移是显然的:

\[\begin{align*} mn_u&\leftarrow \begin{cases} dep_u, & u<mid\\ +\infty, & u\geq mid \end{cases}\\ mn_u&\leftarrow \min(mn_u,\min_{v\in son_u} mn_v) \end{align*} \]

这部分同样是 \(\mathcal{O}(n)\) 的。

考虑 \(g\) 的转移,第一眼的想法会是取 \(u\)\(B\) 邻域内所有 \(f\) 值的与。但这是错误的!因为若 Bob 在 \(u\) 操作完后来到了 \(v\) 节点处,使得 \(v\)\(u\) 的祖先,那么 Alice 在 \(v\) 节点处操作时就不能走 \(u\) 节点对应的 \(v\) 的儿子所在的子树了。

这启发我们再设计一个 DP 数组 \(h\),令 \(h_u\) 表示 Alice 从 \(fa_u\) 开始操作,不能走向 \(sub_u\),会走到哪个点。

这样我们就可以转移 \(g\) 了。转移分为两部分:一部分是 \(u\)\(B\) 邻域内,不为 \(u\) 的祖先的节点的 \(f\) 值的与,一部分是 \(u\)\(u\)\(B-1\) 级祖先的路径上 \(h\) 值的与。

但是这样转移 \(g\) 时会不会有后效性呢?如果我们直接 DFS 树形 DP,是会有的。所以对于这题,我们要按节点的深度从大到小的顺序 DP,然后\(g_u\) 挂在 \(u\)\(A\) 级祖先处更新,这样由于 \(A>B\),我们在更新 \(g_u\) 时,其所需要用来转移的信息都已经处理好了。

再来考虑 \(h\) 的转移,其实和 \(f\) 是类似的。我们\(h_u\) 挂在 \(fa_u\) 处更新。转移时同样要考虑此时 Alice 能否操作:

  • Alice 能操作:我们遍历 \(fa_u\)\(A\) 级儿子,对它们的 \(f\) 值求和得到 \(sum\)。这样扣除 \(sub_u\) 的贡献时,只需用 \(sum\) 减去 \(sub_u\) 内的 \(f\) 值的和,然后判断差是否 \(>0\)。这里需要预处理每个点 \(u\) 位于其 \(A\) 级祖先的哪个子树中,同样可以通过预处理时的 DFS 栈得到。
  • Alice 不能操作:和 \(f\) 类似,我们需要求出 \(sub_{fa_u}\) 内扣除 \(sub_u\) 后深度最浅的满足 \(g_v=0\) 的点 \(v\),转移 \(mn\) 时顺便记录非严格次小值即可。

需要注意 Alice 何时不能操作:除了 \(fa_u\) 不存在 \(A\) 级儿子的情况外,还有一种情况 \(fa_u\)\(A\) 级儿子全部位于 \(sub_u\) 内。这也是好判的。

至此,我们得到了 \(\mathcal{O}(n^2\log{n})\) 的解法。

考虑优化,显然瓶颈在于 \(g\) 的转移。可以把 \(g_u\) 的转移拆成两部分:

  • 对不是 \(u\) 祖先的点 \(v\),求离 \(u\) 最近的使得 \(f_v=0\) 的点 \(v\) 的距离。
  • \(u\)\(u\)\(B-1\) 级祖先的路径上是否存在点 \(v\) 使得 \(h_v=0\)

直接做看起来没前途。我们不妨转换思路,考察每个点 \(v\)\(g_u\) 的影响。

当处理深度为 \(d\) 的点时,我们会更新深度为 \(d+A\) 处的 \(g\) 值,此时 \(g\) 用到的点的深度必然 \(\geq d+A-B\)。于是我们在开始处理第一个深度为 \(d\) 的点时,加入所有深度为 \(d+A-B\) 的点来更新它们的影响。

对于第一种转移,我们注意到 \(\operatorname{lca}(u,v)\) 必然处于 \(u\)\(u\)\(B-1\) 级祖先的路径上,因此考虑在 \(\operatorname{lca}\) 处统计贡献。更具体地,当加入一个节点 \(x\) 时,我们维护出 \(mnf_x\) 表示 \(sub_x\) 内满足 \(g_v=0\) 的点 \(v\) 的最浅深度,然后尝试用它来更新 \(x\)兄弟节点的对应子树,也就是此时我们以 \(fa_x\) 作为 \(\operatorname{lca}\) 统计贡献。我们在 DFS 序上建一颗线段树,把这些兄弟节点对应的 DFS 序区间对 \(mnf_x-2dep_{fa_x}\)\(\min\) 即可。这样子在求离点 \(u\) 最近的 \(f_v=0\) 的点的距离时,只需在将段树上单点查询得到的值加上 \(dep_u\) 即可得到真实值。注意 \(x\) 的兄弟节点对应的 DFS 序区间可以被拆分成至多 \(2\) 个连续区间。

第二种转移其实是简单的。当加入一个节点 \(x\) 时,我们遍历其所有儿子 \(y\),若 \(h_y=0\),我们就把 \(y\) 对应的 DFS 序区间对 \(-\infty\)\(\min\),使得子树内的 \(g\) 全部置为 \(0\)。注意这里会有多余的、深度更大的位置的 \(g\) 值被置为 \(0\),但是由于这些位置的 \(g\) 值我们前面已经计算出来了,所以实际上没有影响。

于是,转移 \(g_u\) 时,我们只需将线段树上单点查询得到的值加上 \(dep_u\) 后和 \(mnf_u-dep_u\)\(\min\),判断其是否 \(>B\) 即可。

线段树需要支持区间 \(\operatorname{chkmin}\) 和单点查询,使用标记永久化即可。

于是我们成功把 check 部分的复杂度优化到了 \(\mathcal{O}(n\log{n})\)。总体时间复杂度为 \(\mathcal{O}(n\log^2{n})\)

代码

仔细想清楚你要做什么,代码就不会很难写了。

这里放出代码的主要部分,未经刻意卡常即可通过本题。

struct SegTree {
#define ls(p) (p << 1)
#define rs(p) (p << 1 | 1)
	int tg[N << 2];
	inline void clr() { fill(tg + 1, tg + (n << 2) + 1, INF); }
	inline void upd(int p, int l, int r, int x, int y, int v) {
		if (x <= l && y >= r) return chk_min(tg[p], v), void();
		int mid = l + r >> 1;
		if (x <= mid) upd(ls(p), l, mid, x, y, v);
		if (y > mid) upd(rs(p), mid + 1, r, x, y, v);
	}
	inline int query(int p, int l, int r, int x) {
		if (l == r) return tg[p];
		int mid = l + r >> 1;
		return min(tg[p], x <= mid ? query(ls(p), l, mid, x) : query(rs(p), mid + 1, r, x));
	}
#undef ls
#undef rs
} sgt;

inline void dfs_pre(int x) {
	stk[dep[x] = ++top] = x, ldfn[x] = ++stmp;
	if (top > A) son[stk[top - A]].push_back(x), ++cnts[bel[x] = stk[top - A + 1]];
	for (int i = t.head[x]; ~i; i = t.nxt[i]) {
		int y = t.to[i];
		if (y == fa[x]) continue;
		fa[y] = x, dfs_pre(y);
	}
	--top, rdfn[x] = stmp;
}
inline bool check(int mid) {
	fill(sum + 1, sum + n + 1, 0), sgt.clr();
	for (int i = 1, k = 1; i <= n; ++i) {
		int x = p[i];
		if (dep[x] ^ dep[p[i - 1]]) {
			int dd = dep[x] + A - B;
			while (k <= n && dep[p[k]] == dd) {
				int y = p[k], fy = fa[y];
				for (int j = t.head[y]; ~j; j = t.nxt[j]) {
					int s = t.to[j];
					if (s == fy) continue;
					if (!h[s]) sgt.upd(1, 1, n, ldfn[s], rdfn[s], -INF);
				}
				int val = mnf[y] - dep[fy] * 2;
				int l = ldfn[fy] + 1, r = rdfn[fy];
				if (l < ldfn[y]) sgt.upd(1, 1, n, l, ldfn[y] - 1, val);
				if (rdfn[y] < r) sgt.upd(1, 1, n, rdfn[y] + 1, r, val);
				++k;
			}
		}
		mn0[x] = x < mid ? dep[x] : INF;
		int smn = INF;
		for (int j = t.head[x]; ~j; j = t.nxt[j]) {
			int y = t.to[j];
			if (y == fa[x]) continue;
			if (mn0[y] < mn0[x]) smn = mn0[x], mn0[x] = mn0[y];
			else chk_min(smn, mn0[y]);
		}
		if (son[x].empty()) {
			f[x] = mn0[x] - dep[x] > B, mnf[x] = !f[x] ? dep[x] : INF;
			for (int j = t.head[x]; ~j; j = t.nxt[j]) {
				int y = t.to[j];
				if (y == fa[x]) continue;
				int val = mn0[y] == mn0[x] ? smn : mn0[x];
				h[y] = val - dep[x] > B;
				chk_min(mnf[x], mnf[y]);
			}
		} else {
			int cnt = 0;
			for (int y : son[x]) {
				g[y] = min(mnf[y] - dep[y], sgt.query(1, 1, n, ldfn[y]) + dep[y]) > B;
				cnt += g[y], sum[bel[y]] += g[y];
			}
			f[x] = cnt, mnf[x] = !f[x] ? dep[x] : INF;
			for (int j = t.head[x]; ~j; j = t.nxt[j]) {
				int y = t.to[j];
				if (y == fa[x]) continue;
				chk_min(mnf[x], mnf[y]);
				if (cnts[y] < son[x].size()) h[y] = cnt - sum[y];
				else {
					int val = mn0[y] == mn0[x] ? smn : mn0[x];
					h[y] = val - dep[x] > B;
				}
			}
		}
	}
	return f[rt];
}
posted @ 2025-08-23 23:04  P2441M  阅读(11)  评论(0)    收藏  举报