1 条题解

  • 0
    @ 2025-8-24 22:31:23

    自动搬运

    查看原文

    来自洛谷,原作者为

    avatar Legitimity
    Will there be a happily-ever-after?

    搬运于2025-08-24 22:31:23,当前版本为作者最后更新于2021-05-10 21:15:46,作者可能在搬运后再次修改,您可在原文处查看最新版

    自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多

    以下是正文


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


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


    subtask1+2+3(?)+5

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

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

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

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

    处理到叶子节点 uu 时,把 uu 删除,按照 fuf_uuu 的祖先的 sizsiz 全减一,找到祖先中 sizsiz 最小的节点 vv,则 fau=vfa_u=v。因为如果 uu 的祖先 vv 不是 uu 的父亲的话,那么 uu 真正的父亲 vv' 的子树则一定是 vv 的子树的子集,所以 sizv<sizvsiz_{v'}<siz_v

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

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

    #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)\Theta(n\log n),反之,如果为链,每次子树只递减 11,那么时间复杂度和询问次数约为 Θ(n2)\Theta(n^2)。数据随机的情况为 Θ(nn)\Theta(n\sqrt n)。(prufer 序列生成的真随机,下文亦同)

    期望得分 [35,55][35,55]


    subtask1+2+3+4

    考虑询问 11

    开这样几个东西:

    1. depidep_i 表示节点 ii 的深度(定义根节点深度为 00
    2. 没了

    disu,v=1dis_{u,v}=1depu>depvdep_u>dep_v 时,fau=vfa_u=v

    (这不用我解释吧……)

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

    代码:

    #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)\Theta(n^2),但其实可以做到和询问次数同阶的。询问次数则与每一次的节点个数有关,询问次数的总数大约是 depi×depi+1\sum dep_i\times dep_{i+1}。如果是链,询问次数达到最优的 nn,如果每次的节点很多,如完全二叉树,那么询问次数将会退化到 n2n^2 以上。随机数据的询问次数也是 nnn\sqrt n 左右。

    期望得分 [55,55][55,55]


    接下来是乱搞环节,我们发现 n2000n\leq 2000 相对于 10510^5 来说非常小,考虑一些不太正经的解法。

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

    期望得分 [100,100][100,100]


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

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

    期望得分 [70,100][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;
    }
    
    • 1

    信息

    ID
    6515
    时间
    1000ms
    内存
    128MiB
    难度
    5
    标签
    递交数
    0
    已通过
    0
    上传者