CF2006E

· · 题解

前言

这篇题解是暴力数据结构卡过去的,没有技术含量 2\log 很不优雅。

前置知识。

题意

给出一棵以 1 为根的树,满足 fa_i < i。

对于每个 i 询问对于由 1 \dots i 节点构成的无根树,能被最小深度为多少的满二叉树包含。

思路分析

记由 1 \dots i 构成的子树为 T_i,下标在下文中会被省略,简记为 T。

记两个点 u,v 的距离为 dis_{u,v},表示两个点之间的简单路径经过对的边的数量。

记 D_u=\max_{v\not = u} dis_{u,v}。

1.观察满二叉树的性质

  1. 从每个点的度数看:叶子的度数为 1,根的度数为 2,其他的度数都是 3。
  2. 从直径看,深度为 d 的满二叉树的直径是 2(d-1)(直径为 \max_{i,j}dis_{i,j})

2.无解情况

考虑存在 i\in T 满足 deg_i > 3,那么这颗树一定不可能被满二叉树包含(满二叉树不存在度数大于 3 的点),直接输出 -1 即可。

3. 考虑其他情况的构造

若任意 i\in T 都有 deg_i \leq 3,该怎么构造一个合法的满二叉树包含他呢?

随便找一个度数小于等于 2 的点 u 当作根节点,暴力将内部每个点的左右儿子补全到同一深度就行,显然这一定是以 u 为根的最小满二叉数。

4. 最优化构造结果

注意到以 u 为根构造的满二叉树的最小深度为 D_u+1。

所以最后对于 T_i 的最小满二叉数覆盖 = \min_{deg_u\leq 2}D_u+1。

5. 怎么求结果

首先观察 D_u 有性质: 到一个点最远的点一定是树直径的端点不知道性质的可以去做这个题

拓展一下:从 u 到 D_u 对应的 v 一定经过了树的直径至少一半的距离(也可以说是与至少一半的直径重合) ::::info[感性证明] 树上证明,正难则反,考虑反证

假设存在 u 到 D_u 对应的 v(后面全部简记为 v),与树的直径的交集小于树直径长度的一半(设另一个直径的端点记作 w,u 到 v 路径上第一次到树直径上的点记作 x)

可以发现 dis_{x,w} \geq 树直径距离的一半 \geq dis_{x,v}。

所以 dis_{u,v} \leq dis_{u,w} 与 v 是 D_u 对应的点矛盾(w 才是对应点)

所以命题不成立。

讨论直径长度的奇偶性

综上所述:只要我们能维护 \min_{deg_u\leq 2}dis_{u,w} 那这道题就可以轻松解决。

数据结构维护信息

考虑从 T_i \to T_{i+1} 有且仅有 deg_{fa_{i+1}} 与 deg_{i+1} 变大 1。

所以问题可以转化为维护一颗树,每次标记一个点/删除一个点的标记,求从某个点到最近的标记点的距离是多少。

这个问题可以说是淀粉树的板子。

所以淀粉树暴力维护就可以了,也就是说 \min\{ 分治中心 x 到最近标记点的距离 + 分治中心 x 到 u 的距离 \}。

代码

时间复杂度 O(n\log^2n),空间复杂度 O(n\log n)。

::::info[代码] ```cpp #include<bits/stdc++.h> using namespace std; #define fi first #define sc second #define pb push_back #define multiTest 1 using pii=pair<int,int>; #define ll long long const int N=500005,LG=20;const ll T=1000000; vector<int>vc[N];int fa[N][LG]; int n,dep[N],dfn[N<<1],st[LG][N<<1],lg[N<<1],dfnpos,deg[N]; inline int getMin(int u,int v){return dep[u]<dep[v]?u:v;} inline void initST(int sz){ for(int i=1;i<=lg[sz];i++)for(int j=1;j+(1<<i)-1<=sz;j++){ st[i][j]=getMin(st[i-1][j],st[i-1][j+(1<<(i-1))]); } } inline int LCA(int u,int v){ u=dfn[u],v=dfn[v];if(u>v)swap(u,v);int len=v-u+1; return getMin(st[lg[len]][u],st[lg[len]][v-(1<<lg[len])+1]); } inline void initLCA(int u,int f){ st[0][dfn[u]=++dfnpos]=u,dep[u]=dep[f]+1; for(int i=1;i<LG;i++)fa[u][i]=fa[fa[u][i-1]][i-1]; for(auto v:vc[u])if(v!=f){ initLCA(v,u); st[0][++dfnpos]=u; } if(!f)initST(dfnpos); } inline int getDis(int u,int v){ return dep[u]+dep[v]-(dep[LCA(u,v)]<<1); } bool vistag[N]; struct NODE{ int dep,fa; priority_queue<ll,vector<ll>,greater<ll> >que; inline void add(int w,int u){ que.push(w*T+u); } inline void del(int w){ vistag[w%T]=1; } inline int solve(){ while(que.size()&&vistag[que.top()%T])que.pop(); if(que.empty())return N; return que.top()/T; } }dft[N]; int dis[LG][N],vis[N],Tdep[N],Tsz[N]; inline void solveDis(int u,int f,int stp,int d=0){ dis[stp][u]=d; for(auto v:vc[u])if(v!=f&&!vis[v])solveDis(v,u,stp,d+1); } inline int findH(int u,int f,vector<int>&point){ Tdep[u]=Tdep[f]+1,Tsz[u]=1,point.pb(u); for(auto v:vc[u])if(v!=f&&!vis[v]){ Tsz[u]+=findH(v,u,point); }return Tsz[u]; } inline void build(int u,int f){ vector<int>point; int tot=findH(u,0,point);u=0; for(auto v:point)if((Tsz[v]<<1)>=tot&&Tdep[v]>=Tdep[u]) u=v; dft[u].dep=dft[f].dep+1,dft[u].fa=f; solveDis(u,0,dft[u].dep),vis[u]=1; for(auto v:vc[u])if(!vis[v])build(v,u); } inline void modifyDft(int u,bool op=true){ if(!op){vistag[u]=1;return;} int p=u;while(p){ if(op)dft[p].add(dis[dft[p].dep][u],u); p=dft[p].fa; } } inline int query(int u){ int p=u,res=N; while(p){ res=min(res,dft[p].solve()+dis[dft[p].dep][u]); p=dft[p].fa; }return res; } inline pii addP(pii u,int v){ int dis1=getDis(u.fi,u.sc),dis2=getDis(u.fi,v),dis3=getDis(u.sc,v); if(dis1>=max(dis2,dis3))return u; if(dis2>=dis3)return pii(u.fi,v); return pii(u.sc,v); } inline pii getMid(int u,int v,int &len){ int tmp=getDis(u,v);len=tmp; if(dep[u]<dep[v])swap(u,v); tmp>>=1; int p=u; while(tmp){ p=fa[p][__builtin_ctz(tmp)]; tmp=tmp&(tmp-1); } if(len&1)return pii(p,fa[p][0]); return pii(p,0); } void test(){ cin>>n; for(int i=1;i<=n;i++){ vc[i].clear(),vis[i]=0; vistag[i]=0; while(dft[i].que.size())dft[i].que.pop(); }dfnpos=0; for(int i=2;i<=n;i++){ cin>>fa[i][0]; vc[fa[i][0]].pb(i); vc[i].pb(fa[i][0]); } initLCA(1,0),build(1,0); cout<<1<<' ';modifyDft(1);deg[1]=0; pii path=pii(1,1); for(int i=2,flag=1,len;i<=n;i++){ if(!flag){cout<<"-1 ";continue;} deg[fa[i][0]]++,deg[i]=1,modifyDft(i); if(deg[fa[i][0]]>3){flag=0;cout<<"-1 ";continue;} if(deg[fa[i][0]]==3)modifyDft(fa[i][0],0); path=addP(path,i); pii mid=getMid(path.fi,path.sc,len); if(len&1)len=len/2+1;else len=len/2; int ans=len+min(query(mid.fi),query(mid.sc)); cout<<ans+1<<" "; }cout<<'\n'; } signed main(){ ios::sync_with_stdio(false),cin.tie(0),cout.tie(0); int t_=1; #if multiTest cin>>t_; #endif lg[0]=-1;for(int i=1;i<=T;i++)lg[i]=lg[i>>1]+1; while(t_--)test(); return 0; } ``` :::: ## 后记 这题有点卡常,multiset 没冲过去,改 priority_queue 才过的。 ::::info[**进阶优化——$O(n\log n)$ 的时间复杂度**] 下面基本是与 GPT 的讨论后思考;推荐写完后再看。 ### 思路一 GPT 说这个东西可以用 top tree 做到严格 $O(n\log n)$ 但这里不讨论这个。 ### 思路二 **写一下 GPT 给的另一个思路**。 注意到时间复杂度瓶颈在淀粉树的插入,删除查询中。 考虑怎么将这个东西优化掉——**分层位集**。 这道题的淀粉树有很优美的值域性质。 - 对于所有分治中心 **对应子树内的最大距离** 之和是 $O(n\log n)$;尝试利用这个性质。 记到分治中心 $u$ 的最大距离为 $d$ 的点的个数为 $cnt_{u,d}$。 记 $vis_i=[cnt_{u,i}\geq1]$。 然后将 $vis$ 按照 **$64$ 位**为一组进行压缩,压缩 $log_{64}n \approx 3$ 次。 求第一个非 $0$ 值可以用 __builtin_ctz 来求。 ### 开始分析复杂度 1. 时间复杂度 - 插入一个值,只会改变一个 $cnt$ 并向上修改 $3$ 次;删除同理。 - 查询只需要 $3$ 次 __builtin_ctz。 所以 multiset 的 $O(\log n) \to O(1)$。 综上总时间复杂度 $O(nlogn)$。 2. 空间复杂度 对每个分治中心考虑: 设分治中心 $u$ 的子树大小为 $sz_u$。 - $cnt$ 空间是 $O(sz_u)$。 - 分层位集的空间是 $\frac{sz_u}{64}+\frac{sz_u}{64^2}+\frac{sz_u}{64^3}=O(\frac{sz_u}{w})\approx O(sz_u)$。 综上总空间复杂度 $O(n\log n)$。 ### 总结: ~~阴完了,诗人握持~~ 通过值域优美的性质优化数据结构+位运算操作将 $O(\log n)\to O(1)$。 真实复杂度其实是 $O(\log_wn)$ 在 $n=500000/2,w=64$ 时,这玩意跟 $O(1)$ 没区别。 ::::