【交互题】

给定一个点数小于1000的树,限制12次询问,每次询问给出一个点集S={a1,a2,...an},回答为max{u,v通路间的最大边 | u∈S,v∈S}

求树中最大边

知识点【交互题操作】【dfs序&欧拉序】

 

----------分割线-----------

【思路】

12≈log2(1000),故核心是每次把边对半分割,再进行询问。

1.比较暴力的想法是直接从一个点出发遍历出n/2条边的新树,

  • 若最大边在新树中,令n=n/2,对新树重复上述操作
  • 若最大边不在新树中,则把新树浓缩成一个点,重复上述操作。

这方法直观,但在缩点方面不好维护,这里暂不采用。

 

2.⭐今日学习重点:

传送门【下方图片未经说明,均来自此奆佬】
1.dfs序即通过dfs的方式遍历整个树。
例如下面的树:
cf746_div2_D_#include
其dfs序为:A-B-D-E-G-C-F-H

通过观察不难发现对于一棵子树它的dfs序是紧挨在一起的,通过加入时间戳还可以确定每个子树所在区间。

  • 这样可以把一些对子树的操作改为对一个区间的操作(当然不可以进行对子树的插入和删除,毕竟这是一个静态的序)
  • 对树进行轻重链划分并用线段树维护dfs序就是树链剖分

2.着重介绍欧拉序:
其与dfs序的区别在于从子树出来时也加入一次序列,可以预见对于一个子树,自身加入一次父辈加入一次,则整个序列长度是小于2n的。
还是上图的树
cf746_div2_D_#include
其欧拉序为A-B-D-B-E-G-E-B-A-C-F-H-F-C-A

欧拉序的最大特点是任取其中一段连续的序,其中的点(可能会有重复)一定能构成一个联通的子树

  • 利用其子树联通性,我们可以通过计入每个点在欧拉序中最早出现的位置(最早中间最迟都可以)找到lca

关于此题:
1.将边的值上升到父节点记录(则意味着叶节点不存在于欧拉序,当然也有存在的写法,两者相差不多)。
2.直接二分询问欧拉序的某段连续子序列,由于欧拉序中点的天然连通性,保证了二分的"是序列"和"否序列"都是一个继续二分的子树。可维护性大大提高。

【codes】

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn=2100;
struct Edge{
	int v,nxt;
}edge[maxn*2];
int n,cnt=0,tot=0,head[maxn],a[maxn],b[maxn];
void add_edge(const int x,const int y){
	edge[tot].v=y,edge[tot].nxt=head[x],head[x]=tot++;
}
void dfs(int u,int fa){
	for(int i=head[u];i!=-1;i=edge[i].nxt){
		if(edge[i].v==fa)continue;
		a[++cnt]=u,b[cnt]=edge[i].v;
		dfs(edge[i].v,u);
		a[++cnt]=u,b[cnt]=edge[i].v;
		
	}
}
int vis[maxn];
inline int query(int l,int r){
	int c=0;
	memset(vis,0,sizeof(vis));
	for(int i=l;i<=r;i++){
		if(!vis[a[i]])vis[a[i]]=1,c++;
		if(!vis[b[i]])vis[b[i]]=1,c++;
	}
	cout<<"? "<<c;
	for(int i=1;i<=n;i++)
		if(vis[i])cout<<" "<<i;
	cout<<endl;
	cin>>c;return c;
}
int main(){
	memset(head,-1,sizeof(head));
	cin>>n;
	for(int i=1;i<n;i++){
		static int x,y;cin>>x>>y;
		add_edge(x,y),add_edge(y,x);
	}
	dfs(1,-1);
	int x=query(1,cnt);
	int l=1,r=cnt;
	while(l<r){
		if(query(l,l+r>>1)==x)r=l+r>>1;
		else l=(l+r>>1)+1;
	}
	cout<<"! "<<a[l]<<" "<<b[l]<<endl;
	return 0;
}