P1035 · OFFICIAL SOLUTION

P1035 双径会深庭 官方题解

Gioush OJ · P1035 双径会深庭

双径会深庭

【题意简述】

给定一棵 nn 个结点的树。每次询问给出 s1,t1,s2,t2s_1,t_1,s_2,t_2,判断路径 (s1,t1)(s_1,t_1) 与路径 (s2,t2)(s_2,t_2) 是否至少经过同一个结点。

【Hint】

【提示】

设两条路径的最近公共祖先分别为 p1,p2p_1,p_2。若两条路径相交,则 p1p_1 位于第二条路径上,或 p2p_2 位于第一条路径上。

【数据点 1∼51\sim 51∼5】

对每次询问分别从 s1s_1s2s_2 出发搜索,恢复两条简单路径,并标记路径上的全部结点。扫描全部结点,只要存在一个结点同时属于两条路径,答案就是 Yes

每次询问需要两次搜索和一次扫描,时间复杂度为 O(nq)\mathcal{O}(nq),空间复杂度为 O(n)\mathcal{O}(n)

【数据点 6∼106\sim 106∼10】

特殊性质保证整棵树是一条链。找到度数为 11 的端点后,沿链确定每个结点的位置。路径 (s,t)(s,t) 便对应位置轴上的闭区间

[min(Pos(s),Pos(t)),max(Pos(s),Pos(t))].[\min(\operatorname{Pos}(s),\operatorname{Pos}(t)),\max(\operatorname{Pos}(s),\operatorname{Pos}(t))].

两条路径相交当且仅当两个闭区间相交。预处理需要 O(n)\mathcal{O}(n) 的时间,每次询问为 O(1)\mathcal{O}(1)

这一档说明,我们不必恢复路径本身,只需要能够用少量端点信息判断一个结点是否位于路径上。

【数据点 11∼1511\sim 1511∼15】

这一档只有 q300q\leq 300,仍然可以使用第一档的路径标记,时间复杂度为 O(nq)\mathcal{O}(nq)

去掉对 qq 的限制后,需要支持两件事:求一条路径中深度最小的结点,以及判断一个结点是否位于给定路径上。这两件事都可以通过最近公共祖先完成。

【正解】

任选结点 11 为根,倍增预处理 LCA\mathbf{LCA}。树上距离为

Dist(u,v)=Depth(u)+Depth(v)2Depth(LCA(u,v)).\operatorname{Dist}(u,v)=\operatorname{Depth}(u)+\operatorname{Depth}(v)-2\operatorname{Depth}(\mathbf{LCA}(u,v)).

结点 xx 位于路径 (u,v)(u,v) 上,当且仅当

Dist(u,x)+Dist(x,v)=Dist(u,v).\operatorname{Dist}(u,x)+\operatorname{Dist}(x,v)=\operatorname{Dist}(u,v).

p1=LCA(s1,t1),p2=LCA(s2,t2).p_1=\mathbf{LCA}(s_1,t_1),\qquad p_2=\mathbf{LCA}(s_2,t_2).

两条路径相交当且仅当 p1p_1 位于路径 (s2,t2)(s_2,t_2) 上,或者 p2p_2 位于路径 (s1,t1)(s_1,t_1) 上。

【正确性证明】

p1p_1 位于第二条路径上,则 p1p_1 同时位于第一条路径与第二条路径,两条路径相交。p2p_2 的情况同理,因此判定的充分性成立。

接下来证明必要性。假设两条路径相交,令 zz 为交集中深度最小的结点。由于 zz 分别位于两条路径上,所以 p1,p2p_1,p_2 都是 zz 的祖先。树上同一结点的祖先构成一条链,因此 p1,p2p_1,p_2 必然具有祖先关系。

不妨设 p1p_1 的深度不小于 p2p_2。那么 p1p_1 位于从 p2p_2zz 的祖先链上。由于 p2,zp_2,z 都位于第二条路径上,二者之间的整段路径也属于第二条路径,所以 p1p_1 位于第二条路径上。另一种深度关系同理,必要性成立。

综上所述,该判定是充要的。

【复杂度分析】

倍增预处理的时间复杂度为 O(nlogn)\mathcal{O}(n\log n)。每次询问只进行常数次最近公共祖先与距离计算,时间复杂度为 O(logn)\mathcal{O}(\log n)。总时间复杂度为 O((n+q)logn)\mathcal{O}((n+q)\log n),空间复杂度为 O(nlogn)\mathcal{O}(n\log n)

【参考代码】

/*- @Author: Ehundategh- @Date: 2023-11-07 15:16:31- @FilePath: \Code\11.6\仓鼠找Sugar.cpp- @Description: You Steal,I Kill */#include <cstdio>#include <cstring>#include <algorithm>#define MAXN 100010using namespace std;int Fa[MAXN][21],Total,Head[MAXN],Depth[MAXN],In1,In2,In3,In4;struct edge{;;;;int St,Ed,Next;}Edge[MAXN<<1];void Edge_Add(int St,int Ed){;;;;Edge[++Total]={St,Ed,Head[St]};;;;;Head[St]=Total;}void DFS(int Now,int From){;;;;Fa[Now][0]=From;;;;;Depth[Now]=Depth[From]+1;;;;;for(int i=Head[Now];i;i=Edge[i].Next){;;;;;;;;int To=Edge[i].Ed;;;;;;;;;if(To==From) continue;;;;;;;;;DFS(To,Now);;;;;}}int LCA(int a,int b){;;;;if(Depth[a]<Depth[b]) swap(a,b);;;;;for(int i=19;i>=0;i--){;;;;;;;;if(Depth[Fa[a][i]]>=Depth[b]) a=Fa[a][i];;;;;};;;;if(a==b) return a;;;;;for(int i=19;i>=0;i--){;;;;;;;;if(Fa[a][i]!=Fa[b][i]){;;;;;;;;;;;;a=Fa[a][i];;;;;;;;;;;;;b=Fa[b][i];;;;;;;;;};;;;};;;;return Fa[a][0];}int Get(int a,int b){;;;;int lca=LCA(a,b);;;;;return Depth[a]+Depth[b]-2*Depth[lca];}bool Query(int a,int b,int c,int d){;;;;int Lca1=LCA(a,b);;;;;int Lca2=LCA(c,d);;;;;int Dis1=Get(a,b);;;;;int Dis2=Get(c,d);;;;;int Judge1=Get(Lca1,c)+Get(Lca1,d);;;;;int Judge2=Get(Lca2,a)+Get(Lca2,b);;;;;if(Judge1==Dis2||Judge2==Dis1) return true;// ;;;;if(LCA(Lca1,Lca2)==Lca2&&(LCA(Lca1,d)==Lca1||LCA(Lca1,c)==Lca1)) return true;// ;;;;if(LCA(Lca1,Lca2)==Lca1&&(LCA(Lca2,a)==Lca2||LCA(Lca2,b)==Lca2)) return true;;;;;return false;}void Solve(){;;;;int n,q;;;;;scanf("%d%d",&n,&q);;;;;Total=0;;;;;memset(Head,0,sizeof(int)*(n+1));;;;;memset(Depth,0,sizeof(int)*(n+1));;;;;memset(Fa,0,sizeof(int)*(n+1)*21);;;;;for(int i=1;i<n;i++){;;;;;;;;scanf("%d%d",&In1,&In2);;;;;;;;;Edge_Add(In1,In2);;;;;;;;;Edge_Add(In2,In1);;;;;};;;;DFS(1,1);;;;;for(int i=1;i<=19;i++){;;;;;;;;for(int j=1;j<=n;j++){;;;;;;;;;;;;Fa[j][i]=Fa[Fa[j][i-1]][i-1];;;;;;;;;};;;;};;;;while(q-->0){;;;;;;;;scanf("%d%d%d%d",&In1,&In2,&In3,&In4);;;;;;;;;printf("%s\n",Query(In1,In2,In3,In4)?"Yes":"No");;;;;}}int main(){;;;;int c,T;;;;;scanf("%d%d",&c,&T);;;;;while(T-->0) Solve();;;;;return 0;}