题解[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})\)
然后就能顺利跑进第一页~