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;
}