题解 P7595 【猜树】&& P7597 【「EZEC-8」猜树 加强版】(交互,构造,dsu on tree)

原题

交互题?可以乱搞,有意思


首先分别考虑只用一种询问的做法。


subtask1+2+3(?)+5

考虑询问 2,把每个节点的子树都搞下来,开这样几个东西:

  1. sizi 表示当前节点 i 子树的大小。
  2. fi 这是一个 vector 或二维数组 ,表示节点 i 所有的祖先(不需要按顺序)

这两个都可以在询问子树的时候求出来,具体做法不细讲。

然后是类似拓扑排序的思想:将所有节点按 siz 由小到大排序,前面的若干个 siz 一定是 1,即这些节点是叶子节点。这样从前到后处理,一定是按照从叶子节点向上处理的顺序。

处理到叶子节点 u 时,把 u 删除,按照 fuu 的祖先的 siz 全减一,找到祖先中 siz 最小的节点 v,则 fau=v。因为如果 u 的祖先 v 不是 u 的父亲的话,那么 u 真正的父亲 v 的子树则一定是 v 的子树的子集,所以 sizv<sizv

这样不断向后处理,处理到 i 时,sizi 也一定等于 1,因为它的子树已经在先前的处理中删掉了。

为什么?可以自己试着画图推一下,其实非常好理解,这里直接放一下代码:

#include<bits/stdc++.h> using namespace std; #define rg register #define inf 0x3f3f3f3f #define ll long long inline int read(){ rg int ret=0,f=0;char ch=getchar(); while(!isdigit(ch)){if(ch=='-')f=1;ch=getchar();} while(isdigit(ch)){ret=ret*10+ch-48;ch=getchar();} return f?-ret:ret; } int n,u,siz[2005],fa[2005]; int f[2005][2005],cnt[2005]; int no[2005]; inline bool cmp(int a,int b){ return siz[a]<siz[b]; } signed main(){ n=read(); siz[1]=n; siz[n+1]=inf; //初始化。 for(rg int i=2;i<=n;++i){ printf("? 2 %d\n",i); fflush(stdout); siz[i]=read(); for(rg int j=1;j<=siz[i];++j){ u=read(); if(u==i) continue; f[u][++cnt[u]]=i; } f[i][++cnt[i]]=1; //1 是根节点,也是所有节点的祖先。 no[i]=i; } sort(no+2,no+1+n,cmp); //将节点按 siz 排序,依次处理。 for(rg int g=2;g<=n;++g){ int now=no[g]; fa[now]=n+1; for(rg int i=1;i<=cnt[now];++i){ --siz[f[now][i]]; //因为自己被删掉了,把祖先的 siz 全部减一。 if(siz[f[now][i]]<siz[fa[now]]) fa[now]=f[now][i]; //siz 最小的就是父亲。 } } printf("! "); for(rg int i=2;i<=n;++i){ printf("%d ",fa[i]); } fflush(stdout); return 0; }

时间复杂度和询问次数?这里的复杂度和子树大小递减的速度有关,同一层的节点越多,复杂度就越低,比如完全二叉树时,时间复杂度和询问次数为 Θ(nlogn),反之,如果为链,每次子树只递减 1,那么时间复杂度和询问次数约为 Θ(n2)。数据随机的情况为 Θ(nn)。(prufer 序列生成的真随机,下文亦同)

期望得分 [35,55]


subtask1+2+3+4

考虑询问 1

开这样几个东西:

  1. depi 表示节点 i 的深度(定义根节点深度为 0
  2. 没了

disu,v=1depu>depv 时,fau=v

(这不用我解释吧……)

那么思路就很明了了,首先询问 1 到其它节点的距离得出 dep,然后每次把已经知道父亲的节点入队(若 depi=1fai=1)。对于对头的节点 v ,询问每个 depv+1=depuu 节点,若 dis1,则令 fau=v ,并把 v 入队。

代码:

#include<bits/stdc++.h> using namespace std; #define rg register #define inf 0x3f3f3f3f #define ll long long inline int read(){ rg int ret=0,f=0;char ch=getchar(); while(!isdigit(ch)){if(ch=='-')f=1;ch=getchar();} while(isdigit(ch)){ret=ret*10+ch-48;ch=getchar();} return f?-ret:ret; } int n,u,fa[2005],dep[2005]; int q[2005],f,r; signed main(){ n=read(); dep[1]=0; for(rg int i=2;i<=n;++i){ printf("? 1 1 %d\n",i); fflush(stdout); dep[i]=read(); if(dep[i]==1){ fa[i]=1; q[++r]=i; } //处理深度和第一批入队的点。 } while(f<r){ int now=q[++f]; for(rg int i=2;i<=n;++i){ if(dep[i]==dep[now]+1&&!fa[i]){ printf("? 1 %d %d\n",now,i); fflush(stdout); u=read(); if(u==1){ fa[i]=now; q[++r]=i; } } }//对于当前节点找到其所有的儿子并入队。 } printf("! "); for(rg int i=2;i<=n;++i){ printf("%d ",fa[i]); } fflush(stdout); return 0; }

时间复杂度和询问次数?因为这题对时间复杂度的要求不高,这里窝偷了个懒,时间复杂度是稳定的 Θ(n2),但其实可以做到和询问次数同阶的。询问次数则与每一次的节点个数有关,询问次数的总数大约是 depi×depi+1。如果是链,询问次数达到最优的 n,如果每次的节点很多,如完全二叉树,那么询问次数将会退化到 n2 以上。随机数据的询问次数也是 nn 左右。

期望得分 [55,55]


接下来是乱搞环节,我们发现 n2000 相对于 105 来说非常小,考虑一些不太正经的解法。

可以按照当前深度的节点数 cnt[depi] 进行分治,如果大于阈值 T,跑第一种,反之跑第二种,时间复杂度是 Θ(n2T+nT)。这是一个显然的根号分治,Tn 时候达到最优的 Θ(nn),不过这里的不再是随机数据,针对所有数据都可以做到复杂度不退化。

期望得分 [100,100]


但还有另一种简单粗暴的写法,不如根号分治优,就是直接判断整颗树的深度,取一个阈值 T,深的话跑第二种,浅的话跑第一种,即数据分治(

我也不知道 T 取多少最好。这样的复杂只是度是比单独的两种好,但还是可以被卡掉,但是这题数据水,这样写就能过了。

期望得分 [70,100]

代码:

#include<bits/stdc++.h> using namespace std; #define rg register #define inf 0x3f3f3f3f #define ll long long #define vit vector<int>::iterator inline int read(){ rg int ret=0,f=0;char ch=getchar(); while(!isdigit(ch)){if(ch=='-')f=1;ch=getchar();} while(isdigit(ch)){ret=ret*10+ch-48;ch=getchar();} return f?-ret:ret; } int n,u,fa[2005],dep[2005],mxdep,siz[2005]; int f[2005][2005],cnt[2005]; int q[2005],fr,r; int no[2005]; inline bool cmp(int a,int b){ return siz[a]<siz[b]; } inline void work(){ siz[1]=n; siz[n+1]=inf; for(rg int i=2;i<=n;++i){ printf("? 2 %d\n",i); fflush(stdout); siz[i]=read(); for(rg int j=1;j<=siz[i];++j){ u=read(); if(u==i) continue; f[u][++cnt[u]]=i; } f[i][++cnt[i]]=1; no[i]=i; } sort(no+2,no+1+n,cmp); for(rg int g=2;g<=n;++g){ int now=no[g]; fa[now]=n+1; for(rg int i=1;i<=cnt[now];++i){ --siz[f[now][i]]; if(siz[f[now][i]]<siz[fa[now]]) fa[now]=f[now][i]; } } } signed main(){ n=read(); dep[1]=0; for(rg int i=2;i<=n;++i){ printf("? 1 1 %d\n",i); fflush(stdout); mxdep=max(dep[i]=read(),mxdep); if(dep[i]==1){ fa[i]=1; q[++r]=i; } } if(mxdep<=50) //数据分治( work(); //第一种。 else{ //第二种。 while(fr<r){ int now=q[++fr]; for(rg int i=2;i<=n;++i){ if(dep[i]==dep[now]+1&&!fa[i]){ printf("? 1 %d %d\n",now,i); fflush(stdout); u=read(); if(u==1){ fa[i]=now; q[++r]=i; } } } } } printf("! "); for(rg int i=2;i<=n;++i){ printf("%d ",fa[i]); } fflush(stdout); return 0; }

加强版

如果你直接把 P7595 的代码往这里一粘,你就可以获得 3050 pts 的优秀分数


此做法来自月赛讲评。

看这题的数据,显然是需要一个 O(nlogn) 的做法。

考虑一个 simple idea:如果 depu=depv+1uv 的子树中,那么 fu=v

那么就有一个显然的做法:n 次询问把 dep 搞出来,然后从根节点向下递归处理每个节点,对于每个节点询问一次子树,这样做询问的量级在 O(n2) 左右(连弱化版都过不了)。

如何改进?对于 v 节点,确定一个儿子 u。知道了 v 节点的子树,又知道了 v 其它儿子的子树,两者相减就可以得到 u 的子树。这样做,每次可以省去一个儿子的子树的询问。

那么 u 应该选哪个呢?显然应该选重儿子,在考虑树剖和 dsu on tree 里的推论,可以得到 sizisizui 的量级在 O(nlogn)(证明可以去看 oiwiki),完全二叉树时达到下界。那么如果能够知道每个节点的重儿子,那么就可以在 O(nlogn) 内解决这个问题。

如何在一颗结构不明的树里找到每个节点的重儿子?随机的艺术。

我们随机一些点,根据定义,重儿子的子树最大,那么应该占随机出的点就应该更多。那么我们就把占随机点最多的儿子当成重儿子。

那么应该随机几个呢?通过实验发现,随机 1 个表现十分优秀。

为什么?随机的艺术。

复杂度的退化主要来自与处理时把重儿子和轻儿子颠倒了,如果出题人想卡这样的做法,就必须将重儿子搞得尽量重,但随即 sizisizui 也会更小,所以实际表现也十分优秀。

代码:(son 表示重儿子,siz 表示子树大小,sontree 表示子树)

#include<bits/stdc++.h> using namespace std; #define rg register #define inf 0x3f3f3f3f #define ll long long inline int read(){ rg int ret=0,f=0;char ch=getchar(); while(!isdigit(ch)){if(ch=='-')f=1;ch=getchar();} while(isdigit(ch)){ret=ret*10+ch-48;ch=getchar();} return f?-ret:ret; } int n,u,dep[5005],siz[5005],sontree[5005][5005],son[5005]; int fa[5005]; bool vis[5005]; void dfs(int x){ if(!siz[x]) return; int tmp=sontree[x][rand()%siz[x]+1],cnt=0; random_shuffle(sontree[x]+1,sontree[x]+siz[x]+1); for(rg int i=1;i<=siz[x];++i){ if(dep[sontree[x][i]]!=dep[x]+1) continue; if(tmp==sontree[x][i]) u=0; else printf("? 1 %d %d\n",sontree[x][i],tmp),fflush(stdout),u=read(); if(u==dep[tmp]-dep[sontree[x][i]]){ son[x]=sontree[x][i]; break; } }//找到重儿子。 memset(vis,0,sizeof(vis)); for(rg int i=1;i<=siz[x];++i){ if(dep[sontree[x][i]]==dep[x]+1&&sontree[x][i]!=son[x]){ printf("? 2 %d\n",sontree[x][i]); fflush(stdout); siz[sontree[x][i]]=read()-1; int cntnow=0; for(rg int j=1;j<=siz[sontree[x][i]]+1;++j){ u=read(); vis[u]=1; if(u!=sontree[x][i]) sontree[sontree[x][i]][++cntnow]=u; } } }//询问轻儿子的子树 for(rg int i=1;i<=siz[x];++i){ if(!vis[sontree[x][i]]&&sontree[x][i]!=son[x]) sontree[son[x]][++siz[son[x]]]=sontree[x][i]; }//根据轻儿子的子树推出重儿子的子树。 for(rg int i=1;i<=siz[x];++i){ if(dep[sontree[x][i]]==dep[x]+1){ fa[sontree[x][i]]=x; dfs(sontree[x][i]); } }//递归处理。 } signed main(){ n=read(); for(rg int i=2;i<=n;++i){ printf("? 1 1 %d\n",i); fflush(stdout); dep[i]=read(); sontree[1][++siz[1]]=i; } //处理深度。 dfs(1); printf("! "); for(rg int i=2;i<=n;++i) printf("%d ",fa[i]); puts(""); fflush(stdout); return 0; }

__EOF__

本文作者Legitimity
本文链接https://www.cnblogs.com/tiatto/p/15875606.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   Legitimity  阅读(68)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
点击右上角即可分享
微信分享提示