题目描述

给定一个 $n$ 个点的有根树,其中 $1$ 号顶点为根。

在树中藏了一个点 $x$。为了找到它,你可以对交互库发出两种询问:

你需要在不超过 $36$ 次询问内找到 $x$。保证隐藏的节点 $x$ 在询问的过程中是不变的。

输入格式

第一行包含一个正整数 $n$ ($2 \leq n \leq 2 \times 10^5$),表示树的点数。

接下来的 $n - 1$ 行,每行包含两个用空格隔开的整数 $u, v$ ($1 \leq u, v \leq n$),表示顶点 $u, v$ 之间有一条边。

输出格式

如果你需要进行询问,请输出 d us u,具体意义见题目描述。

当你已经找到隐藏的节点 $x$ 时,输出 ! x 并退出程序即可。

题解

由于询问次数只有 $36$ 次 (大约是 $\log$ 级别),那该怎么办呢?

由于这道题也是类似问距离的题目,因此和 [loj6669]Nauuo and Binary Tree 类似,考虑使用树链剖分

由于树的形态是已知的,因此一开始可以直接进行树链剖分,并且可以通过一次询问得到点 $x$ 的深度 $dep_x$。

取一条包含根节点 $1$ 的重链,设重链底部的叶子为 $u$。和那道题类似,通过 "树上两点间的距离公式",询问 $u$ 和 $x$ 的距离后即可得到 $u_l = LCA \left( u, x \right)$ 的深度,从而确定该点。

此时,分两种情况讨论:

  1. 如果 $u_l$ 和 $x$ 等深度,则说明顶点 $u_l$ 即为所求顶点。

  2. 反之,说明 $x$ 在 $u_l$ 的轻边所连向的某个子树中。

    而这回的树并不是二叉树,一个点会有许多子节点,又该怎么处理呢?

    其实这并不难,只需要使用一次操作 s 即可得到它所连向的子节点。

由轻重链剖分的性质,询问次数至多不超过 $O \left( \log n \right)$,总时间复杂度 $O \left( n \right)$。

代码

#include <bits/stdc++.h>
#define EB emplace_back
using std::cin;
using std::cout;
using std::endl;

typedef std::vector <int> vector;
const int N = 200054, M = N * 2;

int n, E = 0, Dans;
int to[M], first[N], next[M];
int dep[N], size[N], prf[N];
vector chain[N];

inline int query(char type, int node) {return cout << type << ' ' << node << endl, cin >> node, node;}

inline void addedge(int u, int v) {
	to[++E] = v, next[E] = first[u], first[u] = E;
	to[++E] = u, next[E] = first[v], first[v] = E;
}

void dfs_pre(int x, int px = 0) {
	int i, y, &z = prf[x]; size[x] = 1;
	for (i = first[x]; i; i = next[i])
		if ((y = to[i]) != px) {
			dep[y] = dep[x] + 1, dfs_pre(y, x);
			size[y] > size[z] ? z = y : 0;
			size[x] += size[y];
		}
	if (z) std::swap(chain[x], chain[z]); chain[x].EB(x);
}

int dfs(int x) {
	int cnt = chain[x].size() - 1, remain = Dans - dep[x], dist = query(100, chain[x].back());
	int i = (cnt + remain - dist) / 2, y = chain[x][i];
	return dep[y] == Dans ? y : dfs(query(115, y));
}

int main() {
	int i, u, v;
	std::ios::sync_with_stdio(false), cin.tie(NULL);
	scanf("%d", &n);
	for (i = 1; i < n; ++i) scanf("%d%d", &u, &v), addedge(u, v);
	Dans = query(100, 1), dfs_pre(1);
	for (i = 1; i <= n; ++i) if (!chain[i].empty()) std::reverse(chain[i].begin(), chain[i].end());
	cout << '!' << ' ' << dfs(1) << endl;
	return 0;
}

坑1:和那题类似,计算点 $x$ 的 "深度" 时还是要注意用原先的深度减去当前根的深度,即 $dep'_x = dep_x - dep_r$。