Living-Dream 系列笔记 第67期
树上倍增:维护 \(dp_{i,j}\) 表示节点 \(i\) 向上移动 \(2^j\) 步所到达的节点编号、区间最值、区间和等信息。
倍增求 LCA:
-
预处理:
-
令 \(dp_{i,j}\) 表示 \(i\) 向上走 \(2^j\) 步所到达的节点。
-
转移:\(dp_{i,j}=dp_{dp_{i,j-1},j-1}\)。
-
初始:\(dp_{i,0}=fa_i\)。
-
-
查询:
-
约定 \(x\) 深度更大。
-
\(x\) 倍增向上跳直到与 \(y\) 同深度。
-
特判 \(x,y\) 是否重叠。
-
\(x,y\) 一起向上跳,但不重叠(若重叠了还跳就不是 LCA 了)。
-
跳完后 \(fa_x\) 即为 LCA。
-
-
性质:
-
LCA 与根有关。
-
树上两点距离与 LCA 无关。
-
设 \(dis_x\) 表示树上节点 \(x\) 到根的距离,
\(LCA(x,y)\) 表示树上两点 \(x,y\) 的 LCA,
则树上两点 \(x,y\) 距离为 \(dis_x+dis_y-dis_{LCA(x,y)}\)。
-
P3379
板子。
code
#include<bits/stdc++.h> using namespace std; const int N=5e5+5,M=31; int n,m,s; vector<int> G[N<<1]; int dep[N],dp[N][M]; void initLCA(int cur,int fa){ dep[cur]=dep[fa]+1; dp[cur][0]=fa; for(int i=1;(1<<i)<=dep[cur];i++) dp[cur][i]=dp[dp[cur][i-1]][i-1]; for(int i:G[cur]) if(i!=fa) initLCA(i,cur); } int queryLCA(int x,int y){ if(dep[y]>dep[x]) swap(x,y); for(int i=20;i>=0;i--) if(dep[dp[x][i]]>=dep[y]) x=dp[x][i]; if(x==y) return x; for(int i=20;i>=0;i--) if(dp[x][i]!=dp[y][i]) x=dp[x][i],y=dp[y][i]; return dp[x][0]; } int main(){ ios::sync_with_stdio(0); cin>>n>>m>>s; for(int i=1,u,v;i<n;i++) cin>>u>>v, G[u].push_back(v), G[v].push_back(u); initLCA(s,0); while(m--){ int x,y; cin>>x>>y,cout<<queryLCA(x,y)<<'\n'; } return 0; }
P3398
因为树上节点不会有多个父节点,因此要么 \(LCA(a,b)\) 落在路径 \(c \to d\) 上,要么 \(LCA(c,d)\) 落在路径 \(a \to b\) 上。
检查一个点是否落在一条路径上,就检查该点到路径两端点的距离之和是否为路径长即可。
code
#include<bits/stdc++.h> using namespace std; const int N=5e5+5,M=31; int n,q; vector<int> G[N<<1]; int dep[N],dp[N][M]; void initLCA(int cur,int fa){ dep[cur]=dep[fa]+1; dp[cur][0]=fa; for(int i=1;(1<<i)<=dep[cur];i++) dp[cur][i]=dp[dp[cur][i-1]][i-1]; for(int i:G[cur]) if(i!=fa) initLCA(i,cur); } int queryLCA(int x,int y){ if(dep[y]>dep[x]) swap(x,y); for(int i=20;i>=0;i--) if(dep[dp[x][i]]>=dep[y]) x=dp[x][i]; if(x==y) return x; for(int i=20;i>=0;i--) if(dp[x][i]!=dp[y][i]) x=dp[x][i],y=dp[y][i]; return dp[x][0]; } int main(){ ios::sync_with_stdio(0); cin>>n>>q; for(int i=1,u,v;i<n;i++) cin>>u>>v, G[u].push_back(v), G[v].push_back(u); initLCA(1,0); while(q--){ int a,b,c,d; cin>>a>>b>>c>>d; int LCAab=queryLCA(a,b); int LCAcd=queryLCA(c,d); int LCAlc=queryLCA(LCAab,c); int LCAld=queryLCA(LCAab,d); int LCAla=queryLCA(LCAcd,a); int LCAlb=queryLCA(LCAcd,b); int DISab=dep[a]+dep[b]-2*dep[LCAab]; int DIScd=dep[c]+dep[d]-2*dep[LCAcd]; int DISlc=dep[LCAab]+dep[c]-2*dep[LCAlc]; int DISld=dep[LCAab]+dep[d]-2*dep[LCAld]; int DISla=dep[LCAcd]+dep[a]-2*dep[LCAla]; int DISlb=dep[LCAcd]+dep[b]-2*dep[LCAlb]; cout<<(DISlc+DISld==DIScd||DISla+DISlb==DISab?"Y\n":"N\n"); } }
P4281
很容易发现一个性质:三个点两两的 LCA 必然只有两种可能(即必然会重合一个)。
简单画个图便可知,那个不一样的 LCA 一定是集合点。
继续看图分析,可得三个点 \(x,y,z\) 到集合点的距离即为:
code
#include<bits/stdc++.h> using namespace std; const int N=5e5+5,M=31; int n,m; vector<int> G[N<<1]; int dep[N],dp[N][M]; void initLCA(int cur,int fa){ dep[cur]=dep[fa]+1; dp[cur][0]=fa; for(int i=1;(1<<i)<=dep[cur];i++) dp[cur][i]=dp[dp[cur][i-1]][i-1]; for(int i:G[cur]) if(i!=fa) initLCA(i,cur); } int queryLCA(int x,int y){ if(dep[y]>dep[x]) swap(x,y); for(int i=20;i>=0;i--) if(dep[dp[x][i]]>=dep[y]) x=dp[x][i]; if(x==y) return x; for(int i=20;i>=0;i--) if(dp[x][i]!=dp[y][i]) x=dp[x][i],y=dp[y][i]; return dp[x][0]; } int main(){ ios::sync_with_stdio(0); cin>>n>>m; for(int i=1,u,v;i<n;i++) cin>>u>>v, G[u].push_back(v), G[v].push_back(u); initLCA(1,0); while(m--){ int x,y,z; cin>>x>>y>>z; int LCAxy=queryLCA(x,y); int LCAxz=queryLCA(x,z); int LCAyz=queryLCA(y,z); int ans=0; if(LCAxy==LCAxz) ans=LCAyz; else if(LCAxy==LCAyz) ans=LCAxz; else ans=LCAxy; cout<<ans<<' '<<dep[x]+dep[y]+dep[z]-dep[LCAxy]-dep[LCAxz]-dep[LCAyz]<<'\n'; } return 0; }
CF379F
首先新直径一定得经过新加边(不会更劣),并且新加的两个点是等价的。
于是我们随便选个新加点 \(u\),令原直径端点为 \(x,y\),若 \(dis_{u,x}>dis_{x,y}\) 或 \(dis_{u,y}>dis_{x,y}\),则 \(u\) 可替代 \(x\) 或 \(y\)。
求距离时每次新加两个点就单独维护一下 LCA 即可。\(O(q \log n)\)。
code
#include<bits/stdc++.h> using namespace std; const int N=5e5+5,M=31; int m,n=4; int dep[N<<1],dp[N<<1][M]; vector<int> G[N<<1]; void preLCA(int cur,int fa){ dp[cur][0]=fa; for(int i=1;(1<<i)<=dep[cur];i++) dp[cur][i]=dp[dp[cur][i-1]][i-1]; } int queryLCA(int x,int y){ if(dep[y]>dep[x]) swap(x,y); for(int i=20;i>=0;i--) if(dep[dp[x][i]]>=dep[y]) x=dp[x][i]; if(x==y) return x; for(int i=20;i>=0;i--) if(dp[x][i]!=dp[y][i]) x=dp[x][i],y=dp[y][i]; return dp[x][0]; } int main(){ ios::sync_with_stdio(0); dep[1]=1; for(int i=2;i<=4;i++){ G[1].push_back(i); dep[i]=2; preLCA(i,1); } cin>>m; int x=2,y=3,ans=2; while(m--){ int u; cin>>u; for(int i=1;i<=2;i++){ n++; G[u].push_back(n); G[n].push_back(u); dep[n]=dep[u]+1; preLCA(n,u); } int LCAnx=queryLCA(n,x); int LCAny=queryLCA(n,y); int DISnx=dep[n]+dep[x]-2*dep[LCAnx]; int DISny=dep[n]+dep[y]-2*dep[LCAny]; if(DISnx<=ans&&DISny<=ans) cout<<ans<<'\n'; else if(DISnx>ans){ cout<<DISnx<<'\n'; ans=DISnx,y=n; } else{ cout<<DISny<<'\n'; ans=DISny,x=n; } } return 0; }
P8972
首先必须将边权都转为整数。
然后我们发现若干个小数相乘能为整数,则必须满足它们分解质因数后 \(2\) 或 \(5\) 中最少的个数必须 \(\ge\) 小数位数之和,这样才能抵消掉所有小数位数。
于是我们在 LCA 中维护 \(cnt2_x,cnt5_x,dis_x\) 分别表示节点 \(x\) 到根节点的 边权的质因子中 \(2\) 的个数、\(5\) 的个数,以及小数位数和,然后依上述条件判断即可。
code
#include<bits/stdc++.h> #define int long long using namespace std; const int N=5e5+5,M=31; const int INF=1e16; int n,q,a[N]; int cnt2[N],cnt5[N]; struct E{ int v,w,dot; }; vector<E> G[N<<1]; int dep[N],dp[N][M],dis[N]; int cntdiv(int x,int y){ if(!x) return INF; int res=0; while(x&&x%y==0) x/=y,res++; return res; } void initLCA(int cur,int fa){ dep[cur]=dep[fa]+1; dp[cur][0]=fa; for(int i=1;(1<<i)<=dep[cur];i++) dp[cur][i]=dp[dp[cur][i-1]][i-1]; for(auto i:G[cur]){ if(i.v!=fa){ dis[i.v]=dis[cur]+i.dot; cnt2[i.v]=cnt2[cur]+cntdiv(i.w,2); cnt5[i.v]=cnt5[cur]+cntdiv(i.w,5); initLCA(i.v,cur); } } } int queryLCA(int x,int y){ if(dep[y]>dep[x]) swap(x,y); for(int i=20;i>=0;i--) if(dep[dp[x][i]]>=dep[y]) x=dp[x][i]; if(x==y) return x; for(int i=20;i>=0;i--) if(dp[x][i]!=dp[y][i]) x=dp[x][i],y=dp[y][i]; return dp[x][0]; } signed main(){ ios::sync_with_stdio(0); cin>>n>>q; for(int i=1;i<=n;i++) cin>>a[i]; for(int i=1;i<n;i++){ int u,v; double w; cin>>u>>v>>w; int dot=0; while(w!=floor(w)) w*=10.0,dot++; G[u].push_back({v,floor(w),dot}), G[v].push_back({u,floor(w),dot}); } initLCA(1,0); while(q--){ int x,y; cin>>x>>y; int LCAxy=queryLCA(x,y); int DISxy=dis[x]+dis[y]-2*dis[LCAxy]; int two=cnt2[x]+cnt2[y]-2*cnt2[LCAxy]+cntdiv(a[x],2); int five=cnt5[x]+cnt5[y]-2*cnt5[LCAxy]+cntdiv(a[x],5); cout<<(min(two,five)>=DISxy?"Yes\n":"No\n"); } return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现