首页 > 技术文章 > CF1438F - Olha and Igor

jz-597 2021-01-26 19:37 原文

交互题。

一棵高度为\(h\)的满二叉树(\(n=2^h-1\)),编号顺序打乱。你需要通过至多\(n+420\)次询问操作找出这棵树的根。

询问为\((u,v,w)\)表示以\(w\)为根的\(LCA(u,v)\)

\(3\le h\le 18\)


显然询问\((u,v,w)\)相当于找到一个点,使得\(u,v,w\)在这个点的不同子树内。因此询问顺序和返回值无关。

自己的辣鸡想法:维护一棵树,增量构造,假设加入点\(x\),对这棵树重链剖分,每次询问\((S,T,x)\)\(S,T\)表示重链的两段。如果询问结果\(y\)是出现过的点,则可以确定\(x\)\(y\)的轻儿子中,继续下去找,这部分总共最劣是\(O(h)\);如果\(y\)没有出现,就二分出它在哪两个点之间,插进去,并且把\(x\)连向它,这部分最劣是\(O(\lg h)\)。总共的次数\(O(n(h+\lg h))\),显然过不去,但是可以构出整棵树。

正解:搞一个桶。随机\(420\)个三元组\((u,v,w)\)(三者不相同),设返回值为\(t\)。如果\(t\notin \{u,v,w\}\),把它加入桶中。然后把桶中出现次数最多的两个点取出来,它们大概率都是根的儿子;然后用\(O(n)\)次询问就可以问出根是什么。

对于某个点\(t\),记它的三个子树大小为\(s_1,s_2,s_3\),可以贡献到的三元组个数\(count(t)=s_1s_2s_3\)

显然叶子和根的\(count\)\(0\)。剩下的,对于深度为\(i\)的子树,贡献为\((2^{h-i}-1)^2(2^h-2^{h-i+1})\),由于三个东西和一样,所以和尽量一样时取到最大值,这时候\(i=2\),也就是根的两个儿子。


using namespace std;
#include <bits/stdc++.h>
#define N (1<<18)+5
mt19937 ran(time(0)^*new(int));
#define random(l,r) (ran()%((r)-(l)+1)+(l))
int h,n;
int c[N];
int query(int u,int v,int w){
	int t;
	printf("? %d %d %d\n",u,v,w);
	fflush(stdout);
	scanf("%d",&t);
	return t;	
}
void answer(int x){
	printf("! %d\n",x);
}
int main(){
	scanf("%d",&h);
	n=(1<<h)-1;
	for (int i=1;i<=420;++i){
		int u,v,w,t;
		do
			u=random(1,n),v=random(1,n),w=random(1,n);
		while (u==v || v==w || u==w);
		fflush(stdout);
		t=query(u,v,w);
		if (t!=u && t!=v && t!=w)
			c[t]++;
	}
	int fir=1,sec=2;
	if (c[fir]<c[sec])
		swap(fir,sec);
	for (int i=3;i<=n;++i)
		if (c[i]>c[fir])
			sec=fir,fir=i;
		else if (c[i]>c[sec])
			sec=i;
	for (int i=1;i<=n;++i)
		if (i!=fir && i!=sec){
			int t=query(fir,sec,i);
			if (t==i){
				answer(i);
				return 0;
			}
		}
	return 0;
}

推荐阅读