题解[P7854 GCD Tree]

原题链接

题意:给定 \(n\) 个点,点有点权,求一棵树满足对所有点对 \((i,j)\)\(\gcd(a_i,a_j)=a_{\operatorname{lca}(i,j)}\)

首先,如果一些点的点权相同那它们就能缩成一个点,内部形成一条链。

所以只需看去重后的点如何构造,以下分析都是基于去重后的序列

先求出所有点的 \(\gcd\) ,如果序列中没有这个值,就一定不合法,

否则就是整棵树的根 \(root\) 且点权最小。

由于每个点 \(x\) 作为 \((i,j)\)\(\operatorname{lca}\) 时有 \(a_x|a_i\)\(a_x|a_j\)

所以点权为 \(a_x\) 的倍数的点一定在 \(x\) 的子树内。

而如果存在两个点 \((u,v)\) 使 \(a_u|a_i\)\(a_v|a_i\)\(a_u\)\(a_v\) 不互为倍数。

这个时候 \(i\) 必须在 \(u\) 的子树内又在 \(v\) 的子树内,
\(u\) 不在 \(v\) 的子树内,\(v\) 不在 \(u\) 的子树内。

矛盾!此时就说明序列就不合法。

换句话说,每个点出现在原序列中的因子必须相互整除。

所以对于一个合法的序列,每个点在树上的父亲的必须是序列中除它外的,能整除它且权值最大的点

这可以以 \(O(\sqrt V)\) 的时间找出。

这样就能初步构造出如果序列合法时其树的形态。

这棵树能保证对任意 \((i,j)\)\(a_{\operatorname{lca}(i,j)}|\gcd(a_i,a_j)\)

赋给每个点另外一个值 \(w_i=a_i/a_{fa_i}\) 也即与他父亲做商,特别的,\(w_{root}=1\)

\(path(i,j)\)\(i\)\(j\) 的路径组成的集合。

那么每个点 \(i\)\(a_i=\prod\limits_{j\in path(i,root)}w_j\)

由于 \(a_{\operatorname{lca}(i,j)}=\gcd(a_i,a_j)\)

所以 \(\prod\limits_{k\in path(i,\operatorname{lca}(i,j))}w_k\)\(\prod\limits_{k\in path(j,\operatorname{lca}(i,j))}w_k\) 互素。

也即 \(i\)\(\operatorname{lca}(i,j)\) 路径 \(w\) 积与 \(j\)\(\operatorname{lca}(i,j)\) 路劲 \(w\) 积互素。

所以不满足这个条件时,存在一个 \(d\) 使在两条路径上各有一 \(w\) 是其倍数。

换句话说,树合法时,对每个 \(d\) 是它倍数的 \(w\) 值所对应的点都在树上的一条链上。

记录下每个 \(w\) 值对应树上那些点,可用动态数组或 \(\text{vecotor}\) 实现。

然后枚举 \(d\) ,找出所有它的倍数的 \(w\) 值所对应的点并按深度从小到大排序。

这些点的总个数是 \(O(n\log V)\) 的,因为每个值的因子有 \(O(\log V)\) 个。(为了不影响阅读体验,证明在后面)

对每个点先查询在它之前它到根路径上共有多少点,判断其是否等于之前点的个数。

如不等,则说明其不在一条链上,树就不合法。

查询直接暴力跳就好了,因为由于每个点的父亲权值是其因子,树高至多是 \(\log(V)\) 的。

这样的时间复杂度是 \(O(n\sqrt V+n\log^2 V)\)

对每个值因子个数为 \(O(\log V)\) 的证明:

\(d(n)\)\(n\) 的因子个数,\(n=p_1^{\alpha_1}p_2^{\alpha_2}\dots p_k^{\alpha_k}\)

由于 \(d\) 为积性函数,只需证明对任意的素数 \(p\)\(p^m\) 的因数个数是 \(O(\log p^m)\)

\(d(p^m)=m+1=1+\log_{p} p^m\leq 1+\log p^m\) , 证毕。

代码:

#include<bits/stdc++.h>
using namespace std;
const int N=1e6;
int n,m,x,y,nn,d,tot,res,tmp,sum;char ch;
int a[N+10],f[N+10],dep[N+10],fr[N+10],bk[N+10],size[N+10],t[N+10];
int rev[N+10],f_[N+10],w[N+10],cnt[N+10],*id[N+10];
inline void read(int &x){x=0;ch=getchar();while(ch<48||ch>57)ch=getchar();
	while(ch>47&&ch<58)x=x*10+ch-48,ch=getchar();}
void write(int x){if(x>=10)write(x/10);putchar(48+x%10);}
void dispose(int x){tmp=0;for(y=x;y;y=f_[y])tmp+=size[y];++size[x],++tot;}
bool cmp(int a,int b){return dep[a]<dep[b];}
main(){
	read(n);register int i,j,k;
	for(i=1;i<=n;++i){
		read(a[i]);
		if(!d)d=a[i];
		if(!fr[a[i]])fr[a[i]]=bk[a[i]]=i;
		else f[i]=bk[a[i]],bk[a[i]]=i;
		d=__gcd(d,a[i]);
	}
	for(i=1;i<=N;++i)if(bk[i])a[++nn]=i,rev[i]=nn;
	if(a[1]^d){puts("-1");return 0;}
	dep[1]=1;
	for(i=2;i<=nn;++i){
		m=sqrt(a[i]);
		for(j=2;j<=m;++j)if(a[i]%j==0&&bk[k=a[i]/j]){
			f_[i]=rev[k];dep[i]=dep[rev[k]]+1;f[fr[a[i]]]=bk[k];++cnt[w[i]=j];break;
		}
		if(!f_[i])for(j=m;j;--j)if(a[i]%j==0&&bk[j]){
			f_[i]=rev[j];dep[i]=dep[rev[j]]+1;f[fr[a[i]]]=bk[j];++cnt[w[i]=a[i]/j];break;
		}
	}
	for(i=1;i<=N;++i)if(cnt[i])id[i]=new int [cnt[i]+1],cnt[i]=0;
	for(i=2;i<=nn;++i)id[w[i]][++cnt[w[i]]]=i;
	for(i=2;i<=N;++i){
		for(j=i;j<=N;j+=i)if(cnt[j])for(k=1;k<=cnt[j];++k)t[++sum]=id[j][k];
		sort(t+1,t+sum+1,cmp);
		for(j=1;j<=sum;++j){res=tot;dispose(t[j]);if(tmp^res){puts("-1");return 0;}}
		for(j=1;j<=sum;++j)--size[t[j]];tot=0;
		sum=0;
	}
	for(i=1;i<=n;++i)write(f[i]),putchar(' ');
}

事实上这还能优化不少。

对于一开始 \(O(\sqrt V)\) 的找父亲,可以变成从小到大枚举序列中的每个数的倍数。

这样之前的 \(O(n\sqrt V)\) 处理就优化成 \(O(n\log V)\)

如果这个数的某个倍数存在就更新其父亲,

而后面枚举 \(d\) 时只需枚举素数就足够了。

这部分的粗略的近似应该是 \(O(n\times \dfrac{\ln^2 V}{\ln\ln V})\)

证明有点复杂,放这里

然后就能顺利跑进第一页~

posted @ 2021-09-24 22:19  Y_B_X  阅读(73)  评论(0编辑  收藏  举报