[集训队互测 2022] 在路上 题解

这题的几个 trick 还是比较重要的。

题目传送门:洛谷&QOJ

下面默认 \(\frac{n}{2}\) 下取整。

这种询问树上三个点的关系的题有一个经典策略是拉出一条链来考虑。

我们不妨先思考树是一条链的情况。
首先容易用 \(n\) 次询问找出这条链的两端,具体来讲就是先随便钦定两个点 \(l,r\),然后询问其他点 \(u\),把 \(l,r\) 替换成 \(l,r,u\) 中不是 \(ask(l,r,u)\) 的那两个点。
然后考虑随机二分。
我们在链上每次随机一个点 \(x\),用 \(len\) 次询问(\(len\) 是当前链长)求出它左边和右边的点的个数,如果点数均为 \(\frac{n}{2}\),那它就是重心,否则递归到大的那一边去做。
因为每一次期望删掉 \(\frac{1}{4}\) 的点,所以总复杂度期望是 \(O(n+\frac{3}{4}n+\frac{9}{16}n+...)=O(n)\)。(差不多 \(4n\) 次询问,算上开头那 \(n\) 次刚刚好可以过链的部分分)。

接下来看一般情况。
因为重心的每个子树大小不超过 \(\frac{n}{2}\),所以每次随机两个点 \(x,y\) 重心在这两个点的路径上的概率至少是 \(\frac{1}{2}\)

证明:假设重心的每棵子树大小是 \(a_1,a_2,a_3,...,a_k\),那么重心不在 \(x,y\) 路径上的概率是 \(\frac{\sum_{i=1}^k a_i^2}{n^2}\)
因为 \(\forall i\in [1,k],a_i \le \frac{n}{2}\),所以 \(k\ge 2\),而由于 \(x^2+y^2\le (x+y)^2\),所以要使分子取到最大值 \(k=2\),即 \(a_1=a_2=\frac{n}{2}\),此时概率是 \(\frac{1}{2}\)

所以只需要随机 \(O(1)\) 次就可以找到一条经过重心的链。
我们每次找出随机的链上有可能是重心的点,并 check 它是否真的是重心就可以了。

假设当前的链是 \(x,y\),点集是 \(S\),初始时 \(S\) 是全集,我们用 \(|S|\) 次询问就可以求出哪些点在链上,哪些点挂在 \(x,y\) 下面,哪些点挂在链上其他点的下面。
可能成为重心的点 \(u\) 需要满足他左边的点集(不包括自己) \(L\) 和右边的点集(不包括自己) \(R\) 的大小都 \(\le \frac{n}{2}\)(容易证明一条链上这样的 \(u\) 有且仅有一个),如下图:

我们先判掉端点 \(x,y\)\(u\) 的情况,下面只需要考虑 \(u\) 是链上其他点的情况。
如果我们随机一个 \(u\),那么可以用 \(|S|\) 次询问求出一个点属于 \(L,R\)\(u\) 子树中的哪个,具体来讲对于一个点 \(v\)

  1. 如果 \(ask(x,u,v)\ne u\),那 \(v\) 就属于 \(L\)
  2. 否则如果 \(ask(y,u,v)\ne u\),那 \(v\) 就属于 \(R\)
  3. 其他情况 \(v\) 就属于 \(u\) 子树。

如果 \(L,R\) 的大小都不超过 \(\frac{n}{2}\),那 \(u\) 就是要找的点,否则扔掉小的那部分递归去做。(\(u\) 子树会被保留)
注意: 比如说 \(R> \frac{n}{2}\),那么会扔掉 \(L\),但是虽然之后 \(L\) 内的点不再属于 \(S\),但是他们仍然要考虑到之后的子问题的 \(L\) 中。

但是因为此时树不再是一条链,所以每次随机二分,并不能期望删掉 \(\frac{1}{4}\) 的点(比如这条链上某一边有个点的子树特别大,但是你随到的点一直在另一边),复杂度和询问次数都是错的。
于是我们考虑带权二分,原先每个点被随到的概率都是一样的,现在我们希望一个子树大小是 \(siz\) 的点被随到的概率是 \(\frac{siz}{|S|}\)
但是现在我们并不知道树的结构,也就不知道每个点的 \(siz\),好像没法带权二分。
但是如果我们在 \(S\) 中随机一个点 \(v\),那么这个点是链上 \(u\) 子树内的点的概率正好就是 \(\frac{siz}{S}\)
所以我们只需要每次随机一个点 \(v\) 并找到他所对应的链上的点就完成了带权二分的过程。
要找到 \(v\) 所对应的点 \(u\) 可以先假定 \(u=x\) 然后遍历链上的所有点,当遍历到一个点 \(i\) 时如果 \(ask(v,u,i)=i\) 就令 \(u=i\)

最后剩下的问题就是怎么 check 一个点 \(u\) 是不是重心。
如果 \(u\) 不是重心,那么必然有一个子树的大小 \(>\frac{n}{2}\),所以我们可以考虑用摩尔投票的方法求出这个最大的子树。
回顾如何用摩尔投票求序列绝对众数:设 \(x\) 是绝对众数,并维护一个变量 \(cnt\),初始时令 \(x\gets a_1,cnt\gets 1\),对于序列中的其他数 \(y\),如果 \(x=y\) 就令 \(cnt\gets cnt+1\),否则令 \(cnt\gets cnt-1\),如果此时 \(cnt\) 变成负数就令 \(x\gets y,cnt\gets 1\),如果序列存在绝对众数那最终的 \(x\) 就是绝对众数。
同样的,我们维护一个 \(x\) 表示最大的子树中的点,同时维护一个变量 \(cnt\)。初始 \(x\) 任取一个点,\(cnt=1\),当遍历到一个点 \(y\) 时,如果 \(ask(x,y,u)\ne u\) 意味着 \(x,y\) 在同一个子树,令 \(cnt\gets cnt+1\),否则同理。
最后我们再用 \(n\) 次询问去求出 \(x\) 所在子树的真实大小看一下是否 \(>\frac{n}{2}\) 即可。

询问次数是线性的,常数系数不知道。

code
下面这份代码是以洛谷的交互库为标准的。

#include<bits/stdc++.h>
#define PII pair<int,int>
#define PIII pair<int,pair<int,int> > 
#define fi first
#define se second 
#define eb emplace_back
#define Debug puts("----------------")
using namespace std;
extern "C"  int ask(int x,int y,int z);
int query(int x,int y,int z){
	if(x==y) return x;
	if(x==z) return x;
	if(y==z) return y;
	return ask(x,y,z);
}
mt19937 rnd(20100307);
int n;
bool flag[50005],in_subx[50005],in_suby[50005];
int solve(int x,int y,int sizL,int sizR,vector<int> node){
	vector<int> list,other;  
	int siz_x=0,siz_y=0;
	if(n==49999){
		siz_x=siz_y=1;
		for(int i:node){
			flag[i]=true;
			if(i==x) in_subx[i]=true; else in_subx[i]=false;
			if(i==y) in_suby[i]=true; else in_suby[i]=false;
			if(i!=x&&i!=y) other.eb(i);		
		}	
	}
	else{
		for(int i:node){
			int u=query(x,y,i);
			if(u==x) siz_x++,in_subx[i]=true; else in_subx[i]=false;
			if(u==y) siz_y++,in_suby[i]=true; else in_suby[i]=false;
			if(u==i) list.eb(i),flag[i]=true;
			else flag[i]=false;
			if(!in_subx[i]&&!in_suby[i]) other.eb(i);
		} 
	}
	
	if(node.size()-siz_x+sizR<=n/2) return x;   //排除 x,y 是重心的情况 
	if(node.size()-siz_y+sizL<=n/2) return y;
	
	int v=other[rnd()%(other.size())],u;
	if(flag[v]) u=v;
	else{
		u=x;
		for(int i:list){
			if(i==x) continue;
			if(query(u,i,v)==i) u=i;	
		}
	}
	
	vector<int> L,R,subtree;
	for(int i:node){
		if(in_subx[i]){
			L.eb(i);
			continue;
		}
		if(in_suby[i]){
			R.eb(i);
			continue;
		}
		if(n==49999){
			if(i==u) subtree.eb(i);
			else{
				int z=query(x,u,i);
				if(z==u) R.eb(i);
				else L.eb(i); 
			}
			continue;
		}
		int z=query(x,u,i);
		if(z==u){
			if(query(u,y,i)==u) subtree.eb(i);
			else R.eb(i);
		}
		else L.eb(i);
	}
	if(L.size()+sizL>n/2){
		for(int i:subtree) L.eb(i);
		return solve(x,u,sizL,sizR+R.size(),L);
	}
	else if(R.size()+sizR>n/2){
		for(int i:subtree) R.eb(i);
		return solve(u,y,sizL+L.size(),sizR,R);		
	}
	return u;
}
bool check(int u){
	int cnt=1,x=1,siz=0;
	if(x==u) x++;
	for(int y=x+1;y<=n;y++){
		if(y==u) continue;
		if(query(x,y,u)!=u) cnt++;
		else{
			cnt--;
			if(cnt<0) x=y,cnt=1;
		}
	}
	for(int y=1;y<=n;y++){
		if(y==u) continue;
		if(query(x,y,u)!=u) siz++;
	}
	return siz<=n/2;
}
extern "C"  int centroid(int id,int N,int M){
	if(N==3) return ask(1,2,3);
	map<PII,bool> mp;
	n=N;
	vector<int> all(n);
	for(int i=1;i<=n;i++) all[i-1]=i;
	if(n==49999){  //注意链的情况的特殊数据范围 
		int l=1,r=2;
		for(int i=3;i<=n;i++){
			int u=query(l,r,i);
			if(u==l) l=i;
			else if(u==r) r=i;
		}	
		return solve(l,r,0,0,all);
	} 
	while(true){
		int x=rnd()%n+1,y=rnd()%n+1;
		while(mp[{x,y}]||x==y) x=rnd()%n+1,y=rnd()%n+1;
		mp[{x,y}]=true;
		int u=solve(x,y,0,0,all);
		if(check(u)) return u;
	}
}

弱弱地问一句,有没有人会严格证明这个带权二分的期望复杂度,写的时候突然发现自己不是很会证。

posted @ 2025-05-09 16:02  Green&White  阅读(25)  评论(0)    收藏  举报