Codeforces 1033E. Hidden Bipartite Graph

题目链接:1033E - Hidden Bipartite Graph

题目大意:交互题,有一个点数为 \(n\le 600\) 的无向连通图(\(n\) 给定),有 \(20000\) 次询问机会。每次询问可以给出一个点集,返回点集内的点两两之间一共有多少条边。要求判断图是否为二分图,若是则输出其中一边,若不是则输出其中一个奇环。

考虑将图分层,一开始第一层对应点集 \(S_1={1}\),之后如果对每一层 \(S_i\),都能找到所有与 \(S_i\) 有边相连的点,那么就能有一个新的点集 \(S_{i+1}\),一直这样下去就能建起一棵生成树,实现对图的分层。

那么现在就需要处理若干个这样的子问题:有点集 \(S,T\),要求在 \(T\) 中找到所有与 \(S\) 有边相连的点。

考虑分治,对点集 \(T\) ,可以把所有点都存在 vector 中,这样每个区间就对应着 \(T\) 中的一个子集。我们把 \(T\) 分成两部分 \(T_L=[l,mid],T_R=(mid,r]\),如果我们能判断出 \(T_L,T_R\)\(S\) 之间的连边关系,对有边的区间继续询问下去,就能够进行递归求解求出所有满足条件的点。

现在相当于我们需要判断两个点集 \(S,t\) 之间是否有连边,那么我们知道,当询问的点集为 \(S\cup t\) 时,返回的边数里包含了 \(S\) 内部以及 \(t\) 内部自身的连边。所以我们相当于要判断 \(Ask(S\cup t)-Ask(S)-Ask(t)\) 的值。而由于在每一层,\(S\) 都是固定的,所以可以提前存下 \(Ask(S)\) 的值,就可以做到两次询问实现判断了。

我们分析一下这样做需要耗费的询问次数。其实可以类比线段树的单点修改操作,对于所有满足条件的点,与之相关的区间都只会有 \(O(\log n)\) 个,于是可以得出总的询问次数就是 \(O(n\log n)\) 的。

现在我们求出了每一层有哪些点,接下去就可以进行判断。只需要把奇数层和偶数层分别放到一起,判断内部是否有连边即可。那么是二分图的情况非常好弄,直接输出就好,不是二分图的情况就不是很好搞,因为我们需要找到一个奇环。

我们知道,出现矛盾是因为奇数层(或偶数层)内部有边相连,那么如果我们能够找到这条边 \((x,y)\),并找到 \(x\)\(y\) 到这棵生成树上 \(\operatorname{LCA}(x,y)\) 的路径,就相当于找到了一个奇环。

那么这里就面临着两个问题:

  • 怎么找到 \((x,y)\)
  • 怎么找到 \(\operatorname{LCA}(x,y)\),或者怎么找一个点 \(x\) 在生成树上的父亲。

首先,我们可以通过枚举集合内部的点 \(i\),并判断 \(i\) 与集合内其他点是否有连边找到其中的一个端点 \(x\)。找到 \(x\) 之后,我们只需要在剩下的集合中找到一个与 \(x\) 相邻的点即可。

而关于找 \(x\) 在生成树上的父亲,我们可以在分层的时候就可以记录每个点对应的层数 $v_x $,那么我们只需要找到一个在 \(v_x-1\) 层的一个点与 \(x\) 相邻即可。如果能实现这一操作,我们就能够一步步往上跳暴力找到两个点的 \(\operatorname{LCA}\)

可以发现,这两个问题最终都需要我们处理一个操作:给定集合 \(T\) 与点 \(x\),找到 \(T\) 中任意一个与 \(x\) 有边相连的点。可以套用之前的做法分治,但是因为这里只需要找到任意一个满足条件的点,二分查找就足够了,这一部分所需要的询问次数也是 \(O(n\log n)\) 的。

于是在最坏情况下,每次操作都需要耗费两个询问次数,大概需要询问 \(4n\log n\) 次,正好卡在 \(20000\) 以内,最后本人代码的最多询问次数为 \(16675\),还是比较充裕的。

#include<bits/stdc++.h>
using namespace std;
#define N 666
int n,cnt,v[N],t;
vector<int>d,s,tmp,O,E,f[N];
int Ask(vector<int>D)//询问 
{
	if(D.size()<2)return 0;//点集大小不超过2时,可以不用询问 
	printf("? %d\n",(int)D.size());
	for(auto i:D)printf("%d ",i);
	printf("\n");
	fflush(stdout);
	int res;
	scanf("%d",&res);
	return res;
}
int Count(int l,int r)//计算S和区间[l,r]内的点之间的边数 
{
	tmp.clear();
	for(int i=l;i<=r;i++)tmp.push_back(d[i]);
	int Self=Ask(tmp);//计算[l,r]内部边数 
	for(auto i:s)tmp.push_back(i);
	int Sum=Ask(tmp);//计算并集的总边数 
	return Sum-Self-t;//t的值在之前已经提前求过了,是S内部的边数 
}
void Find(int l,int r)//利用分治的思想找到每个与S有连边的点 
{
	if(l==r){
		if(Count(l,r)){//单独判断d[l]与S是否有连边 
			v[d[l]]=cnt;//v[i]表示i号结点在第几层 
			f[cnt].push_back(d[l]);//把每一层的结果存下来,后面找父亲要用 
		}
		return;
	}
	int mid=l+r>>1;//这里的操作和线段树单点修改类似 
	if(Count(l,mid))Find(l,mid);
	if(Count(mid+1,r))Find(mid+1,r);
}
int Find2()//已知点x与d内的点有连边,二分找出一个与x有连边的点 
{
	int l=0,r=d.size()-1;
	while(l<r){
		int mid=l+r>>1;
		if(Count(l,mid))r=mid;
		else l=mid+1;
	}
	return d[l];
}
int getf(int x)//对于点x,在树上的父亲一定在v[x]-1这一层 
{
	t=0;
	d=f[v[x]-1];
	s.clear();
	s.push_back(x);
	return Find2();
}
void rua(vector<int>D)//找奇环 
{
	int m=D.size(),x,y;
	for(int i=0;i<m;i++){//通过枚举找到一个与其他点有连边的x 
		tmp.clear();
		for(int j=0;j<m;j++)
			if(j!=i)tmp.push_back(D[j]);
		if(Ask(tmp)<t){
			x=D[i];
			break;
		}
	}
	t=0;
	d=tmp;
	s.clear();
	s.push_back(x);
	y=Find2();//找到一个与x有连边的y 
	vector<int>ansx,ansy;//分别存储x,y到其祖先的路径,直接暴力找LCA 
	ansx.push_back(x);
	ansy.push_back(y);
	while(v[x]<v[y]){
		x=getf(x);
		ansx.push_back(x);
	}
	while(v[y]<v[x]){
		y=getf(y);
		ansy.push_back(y);
	}
	while(x!=y){
		x=getf(x);
		y=getf(y);
		ansx.push_back(x);
		ansy.push_back(y);
	}
	printf("N %d\n",ansx.size()+ansy.size()-1);
	for(auto i:ansx)printf("%d ",i);
	ansy.pop_back();
	reverse(ansy.begin(),ansy.end());//因为之前输出的是x到LCA的路径,所以要翻转 
	for(auto i:ansy)printf("%d ",i);
}
int main()
{
	scanf("%d",&n);
	v[1]=++cnt;
	s.push_back(1);
	f[1].push_back(1);
	for(int i=2;i<=n;i++)d.push_back(i);
	while(!d.empty()){
		cnt++;
		t=Ask(s);//提前计算S内部的连边,减少询问次数 
		Find(0,d.size()-1);
		d.clear(),s.clear();
		for(int i=1;i<=n;i++){
			if(!v[i])d.push_back(i); //所有还没访问过的点作为集合T
			if(v[i]==cnt)s.push_back(i);//把当前层的点作为新的S 
		}
	}
	for(int i=1;i<=n;i++)//奇偶分别存,判断是否存在奇环 
		if(v[i]&1)O.push_back(i);
		else E.push_back(i);
	t=Ask(O);
	if(t){
		rua(O);
		return 0;
	}
	t=Ask(E);
	if(t){
		rua(E);
		return 0;
	}
	printf("Y %d\n",(int)O.size());
	for(auto i:O)printf("%d ",i);
	return 0;
}
posted @ 2022-07-23 21:01  DeaphetS  阅读(53)  评论(0编辑  收藏  举报