题解: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\) 级儿子,容易列出转移方程:
我们可以预处理出 \(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\) 的转移是显然的:
这部分同样是 \(\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];
}

浙公网安备 33010602011771号