[LOJ6**9]Nauuo and Binary Tree

98 篇文章 0 订阅
10 篇文章 0 订阅

题目

传送门 to LOJ

题目描述
这是一道 交互题

有一棵以 1 1 1 为根的树;可以通过询问得知 x , y x,y x,y 的树上距离,请还原这棵树(输出每个点的父节点)。

询问次数应当控制在 n log ⁡ n n\log n nlogn 内(注:原题中是绝对数值,但翻译过来就是这个)。

数据范围与提示
原题: n ⩽ 3000 n\leqslant 3000 n3000 。可行范围: n ⩽ 1 0 5 n\leqslant 10^5 n105

思路

脑洞其实挺大。最重要的是 打开思路(尽管这也只是一句口号)。

最可悲的是,哪怕我做过原题;哪怕这道题有两种思路;哪怕这两种思路都并不离奇;我还是不会做。这就是弱者的悲哀。弱者抱团取暖,因为他们心底都只剩下漆黑的寒冷。

第一种想法是,根据 d i s ( x , y ) = d e p ( x ) + d e p ( y ) − 2 d e p ( l c a ) dis(x,y)=dep(x)+dep(y)-2dep(lca) dis(x,y)=dep(x)+dep(y)2dep(lca),如果提前询问出所有点的深度,询问等价于询问 l c a lca lca 的深度。又根据树是二叉的,就可以知道 y y y l c a lca lca 不含 x x x 的子树中。一直向下,就可以知道 y y y 的确切位置(假如 x x x 是已知的)。

首先要明确的是:要按照深度逐层处理。因为我们要通过 d e p ( l c a ) dep(lca) dep(lca) 就知道 l c a lca lca,肯定说明 x x x 的祖先都要被找到了;同时还要保证 y y y 的祖先都被找到了,否则无法唯一确定。我连这个都没想,直接就开始考虑所谓 “正解”,真是可笑!

此时,我们可以考察一下询问次数。很容易发现,假如 y y y 确定在 r r r 子树内,那么每次询问就是走到链 ⟨ r , x ⟩ \langle r,x\rangle r,x 的一条子链。也就是一个链划分的问题。可以发现,最坏询问次数就是 “虚边” 的数量;所以重链剖分之后,可以做到 n log ⁡ n n\log n nlogn 次询问。

然而,在不知道原树之前,我们无法得知重链剖分结果。吾卒凝滞于此!何必用原树的重链剖分结果?用当前的重链剖分结果就行了啊!重链剖分并不是均摊的结果,而是严格的每次 O ( log ⁡ n ) \mathcal O(\log n) O(logn),所以不需要全局性的考虑!

于是,只需要动态维护重链剖分。一种 简单的偷懒 方法是,每插入 O ( n ) \mathcal O(\sqrt n) O(n ) 个点,暴力重构一次重链剖分;在不重构的时候,“随机链剖分”。不难发现,如果原树是超级左偏树(一条左儿子链,每个点接一个右儿子),在两次重构之间,有 n 2 \sqrt{n}\over 2 2n 条链上边,都是随机剖分的,所以期望是 n 4 \sqrt{n}\over 4 4n 条轻边。这还是略大了些;所以要勤于重构,平衡一下。

老老实实地做,可能就需要引入替罪羊:若重儿子子树大小的占比不小于 α \alpha α,认为重链仍平衡。那么重链上就维护 s i z e ( s o n ) − α ⋅ s i z e ( x ) size(son)-\alpha\cdot size(x) size(son)αsize(x) 即可;新加叶子时,相当于区间加 ( 1 − α ) (1-\alpha) (1α),轻边上端点只减 α \alpha α 。维护 min ⁡ \min min 值,发现 min ⁡ < 0 \min<0 min<0 时则重构即可。

第一个思路就讲完了。第二个思路是,树是二叉的,说明一个点的度数至多是 3 3 3;那么,询问 d i s ( x , y ) dis(x,y) dis(x,y) 后,再询问 d i s ( u , y )    ( ⟨ x , u ⟩ ∈ E ) dis(u,y)\;(\langle x,u\rangle\in E) dis(u,y)(x,uE),就可以确定 y y y 在以 x x x 为根时的哪个子树内。显然我们应该找子树重心。

1 1 1 开始拓展连通块,还是应该按照深度处理,问题变为动态点分树。又是替罪羊登场的时间了……

第三个思路:前面说 “重链剖分不基于均摊”,所以肯定有基于均摊的方法。那就是 LCT \textit{LCT} LCT 呗。访问轻边的数量是均摊 O ( log ⁡ n ) \mathcal O(\log n) O(logn) 的,完了。

代码

这里给出 O ( n ) \mathcal O(\sqrt n) O(n ) 次重建的做法。其他的太难打了

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cctype>
#include <random>
#include <vector>
using namespace std;
typedef long long llong;
# define rep(i,a,b) for(int i=(a); i<=(b); ++i)
# define drep(i,a,b) for(int i=(a); i>=(b); --i)
inline int readint(){
	int a = 0, c = getchar(), f = 1;
	for(; !isdigit(c); c=getchar())
		if(c == '-') f = -f;
	for(; isdigit(c); c=getchar())
		a = (a<<3)+(a<<1)+(c^48);
	return a*f;
}

inline int visit(int x,int y){
	printf("? %d %d\n",x,y);
	fflush(stdout); return readint();
}

int get_sqrt(int n){
	int x = 1; // I hate binary search
	rep(i,1,20) x = (x+(n/x))>>1;
	return x; // Newton Iteration
}

const int MAXN = 100005;
int ch[MAXN][2], siz[MAXN];
void scan(int x){
	drep(j,siz[x]=1,0) if(ch[x][j])
		scan(ch[x][j]), siz[x] += siz[ch[x][j]];
}
int top[MAXN], ass[MAXN];
vector<int> chain[MAXN];
void build(int x,int bel){
	if(!x) return ; // nothing remains
	chain[top[x] = bel].push_back(x);
	if(!ch[x][0] && !ch[x][1]) return void(ass[x] = x);
	int d = (siz[ch[x][1]] > siz[ch[x][0]]);
	build(ch[x][d],bel), ass[x] = ass[ch[x][d]];
	build(ch[x][d^1],ch[x][d^1]); // light son
}

int dep[MAXN]; vector<int> buc[MAXN];
void findtree(int n,int *p){
	buc[0].push_back(1); // for completeness
	rep(i,2,n){
		dep[i] = visit(1,i);
		buc[dep[i]].push_back(i);
	}
	for(int i=0,len=int(buc[1].size()); i!=len; ++i)
		p[ch[1][i] = buc[1][i]] = 1;
	scan(1), build(1,1); // stupid enough
	const int SQRTN = get_sqrt(n);
	for(int d=2,cnt=0; d!=n; ++d){
		for(const int &x : buc[d]){
			int now = 1; // top of chain
			for(int bot=ass[now]; true; bot=ass[now]){
				int lca = dep[bot]+dep[x]-visit(bot,x);
				if((lca >>= 1) == dep[bot]) break;
				now = chain[now][lca-dep[now]];
				int v = (top[ch[now][1]] == top[now]);
				now = ch[now][v^1]; // in light son's subtree
			}
			p[x] = ass[now]; // just record this
			if(ch[p[x]][0]) ch[p[x]][1] = x;
			else ch[p[x]][0] = x; // link child
			++ cnt; // how many operations done
		}
		for(const int &x : buc[d-1]){
			if(!ch[x][0] && !ch[x][1]) continue;
			static mt19937 rnd(114514);
			static bernoulli_distribution _sy(0.5);
			int v = ch[x][1] ? _sy(rnd) : 0;
			ass[top[x]] = ch[x][v], top[ch[x][v]] = top[x];
			chain[top[x]].push_back(ch[x][v]); // new leaf
			if(ch[x][v^1]){ // another become a single node
				top[ch[x][v^1]] = ass[ch[x][v^1]] = ch[x][v^1];
				chain[ch[x][v^1]].push_back(ch[x][v^1]);
			}
		}
		if(cnt > SQRTN){
			for(int _d=0; _d<=d; ++_d)
				for(const int &_x : buc[_d])
					chain[_x].clear(); // clear all
			scan(1), build(1,1), cnt = 0;
		}
	}
}

int fa[MAXN];
int main(){
	int n = readint();
	findtree(n,fa); putchar('!');
	rep(i,2,n) printf(" %d",fa[i]);
	putchar('\n'); fflush(stdout);
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值