二至济南--图论(待补)
\(update: 2025/7/26\)
树
无环无重边且联通的图
即有\(n\)个节点,\(n-1\)条边构成的联通图
分为无根树(无向边)和有根树(有向边)
最近公共祖先(\(LCA\))
对于\(u\)和\(v\),其最近公共祖先位于根节点到\(u\)和根节点到\(v\)的路径上,且距离\(u\)和\(v\)最近
我们有许多做法求\(LCA\),这里介绍三种
倍增法
最近公共祖先的一个难点在于查询,而倍增法就是通过倍增的方法优化记录和查询 (好像一句废话)
记录其第\(2^0\),\(2^1\),\(2^2\)...的祖先,查询时从最远的祖先开始,跳到其记录的最近的祖先
\(code\)
//May all the beauty be blessed.
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,s,f[500010][30],d[500010];
vector<int> a[500010];
void dfs(int x,int fa){
f[x][0]=fa;
d[x]=d[fa]+1;
for(int i=1;i<=__lg(d[x]);i++) f[x][i]=f[f[x][i-1]][i-1];//一个推到公式,它的第2^i个祖先是它第2^{i-1}个祖先的第2^{i-1}个祖先
for(auto i:a[x]) if(i!=fa) dfs(i,x);
}
int lca(int x,int y){
if(d[x]<d[y]) swap(x,y);//默认令x的深度大
while(d[x]>d[y]) x=f[x][__lg(d[x]-d[y])];//从x向上跳,直到与y的深度相同
if(x==y) return x;//特判一下
for(int i=__lg(d[x]);i>=0;i--){ //一起跳父亲
if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];
}
return f[x][0];//结果就是这个,在上面我们只当f[x][i]!=f[y][i]是才跳的父亲,因此他们会在lca的儿子处停下
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>m>>s;
for(int i=1;i<n;i++){
int x,y;
cin>>x>>y;
a[x].push_back(y);
a[y].push_back(x);
}
dfs(s,0);
while(m--){
int x,y;
cin>>x>>y;
cout<<lca(x,y)<<'\n';
}
}
树剖法
虽然关于树剖在下面
树剖中我们发现\(x\)所在的链上(\(x\)的前面)都是\(x\)的祖先,因此可利用此性质让\(x\)和\(y\)不断跳链,最终落到同一条链上,就能找到\(LCA\)
\(code\)
//May all the beauty be blessed.
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,q,rt;
vector<int> a[500010];
int dis[500010],f[500010],son[500010],siz[500010];
int top[500010];//由于求LCA没有用到树剖的很多功能,这里少了不少没用到的数组
//以下是简化版的树剖
void dfs1(int x,int fa){
dis[x]=dis[fa]+1;
f[x]=fa;
for(auto i:a[x]){
if(i==fa) continue;
dfs1(i,x);
siz[x]+=siz[i];
if(siz[son[x]]<siz[i]) son[x]=i;
}
siz[x]++;
}
void dfs2(int x,int topf){
top[x]=topf;
if(son[x]) dfs2(son[x],topf);
for(auto i:a[x]){
if(i==f[x]||i==son[x]) continue;
dfs2(i,i);
}
}
//求LCA
int lca(int x,int y){
while(top[x]!=top[y]){//先跳到同一条链上
if(dis[top[x]]<dis[top[y]]) swap(x,y);
x=f[top[x]];
}
if(dis[x]>dis[y]) swap(x,y);//谁深度小返回谁
return x;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>q>>rt;
for(int i=1;i<n;i++){
int x,y;
cin>>x>>y;
a[x].push_back(y);
a[y].push_back(x);
}
dfs1(rt,0);//记得调用树剖的函数!!!!!!!!!!
dfs2(rt,rt);
while(q--){
int x,y;
cin>>x>>y;
cout<<lca(x,y)<<'\n';
}
}
\(dfs\)序(待补)
听说是两遍\(dfs\)就可以搞定 而且有一堆好处,虽然我没看出来 ,
而且我不会,因此暂时不放
树剖
到底是哪个大天才想出来把树上相关的东西和线段树两个大码量的方法结合在一起的
感性的理解一下就是将树拆成链,通过我们学过的对链(数组)的修改方法 就是你 线段树,
来维护
详解
谈树剖之前,我们先引入 Tarjan中的 \(dfs\) 序
一棵树的 $dfs $序有很好的性质
- \(dfs\) 序其实是这棵树的前序遍历
- 一棵树的 \(dfs\) 序是连续的
对于第二点, 我们可以发现, 以 \(s\) 为根的子树的 \(dfs\) 序其实是连续的, 证明也很简单 (
\(dfs\) 在出子树之前会先把子树全部遍历)
这样, 我们就将拍成了一个序列 (就可以使用线段树了)
但光有这个序列是不够的, 我们希望它能对树上任意两点之间的路径 ( 显然只有一条 ) 进行操作, 于是,我们引入更多东西 ( 也是树剖的重点 )
- 重儿子 : 根节点所连接的所有子树中,子树大小最大的子树的根
- 轻儿子 : 除了重儿子以外的节点
- 重边 : 连接任意两个重儿子的边
- 轻边 : 非重边
- 重链 : 一条以轻儿子开头, 只经过重边的路径所形成的链
应用
功能很多,但这里只介绍几种
节点修改,求和
P 3384
\(code\)
//May all the beauty be blessed.
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,rt,mod,org[100010];
vector<int> a[100010];
int dis[100010],f[100010],siz[100010],son[100010];
int dfn[100010],l,now[100010],top[100010];
int c[100010*4],tag[100010*4];
void dfs1(int x,int fa){
f[x]=fa;
dis[x]=dis[fa]+1;
for(auto y:a[x]){
if(y==fa) continue;
dfs1(y,x);
siz[x]+=siz[y];
if(siz[son[x]]<siz[y]) son[x]=y;
}
siz[x]++;
}
void dfs2(int x,int fa,int topf){
dfn[x]=++l;
now[l]=org[x];
top[x]=topf;
if(!son[x]) return;
dfs2(son[x],x,topf);
for(auto y:a[x]){
if(y==fa||y==son[x]) continue;
dfs2(y,x,y);
}
}
void build(int L,int R,int x){
if(L==R){
c[x]=now[L]%mod;
return;
}
int mid=L+R>>1;
build(L,mid,x<<1);
build(mid+1,R,x<<1|1);
c[x]=c[x<<1]+c[x<<1|1];
c[x]%=mod;
}
void addtag(int L,int R,int s,int x){
c[x]+=s*(R-L+1);
c[x]%=mod;
tag[x]+=s;
tag[x]%=mod;
}
void pushdown(int L,int R,int x){
if(!tag[x]) return;
int mid=L+R>>1;
addtag(L,mid,tag[x],x<<1);
addtag(mid+1,R,tag[x],x<<1|1);
tag[x]=0;
}
void add(int L,int R,int l,int r,int s,int x){
if(l<=L&&R<=r){
addtag(L,R,s,x);
// cout<<L<<" "<<R<<" "<<c[x]<<" "<<'\n';
return;
}
pushdown(L,R,x);
int mid=L+R>>1;
if(l<=mid) add(L,mid,l,r,s,x<<1);
if(r>mid) add(mid+1,R,l,r,s,x<<1|1);
c[x]=c[x<<1]+c[x<<1|1];
c[x]%=mod;
}
int findl(int L,int R,int l,int r,int x){
if(l<=L&&R<=r) return c[x]%mod;
pushdown(L,R,x);
int mid=L+R>>1,sum=0;
if(l<=mid) sum+=findl(L,mid,l,r,x<<1),sum%=mod;
if(r>mid) sum+=findl(mid+1,R,l,r,x<<1|1),sum%=mod;
sum%=mod;
return sum;
}
void cc1(int x,int y,int s){
while(top[x]!=top[y]){
if(dis[top[x]]<dis[top[y]]) swap(x,y);
add(1,n,dfn[top[x]],dfn[x],s,1);
x=f[top[x]];
}
if(dis[x]>dis[y]) swap(x,y);
add(1,n,dfn[x],dfn[y],s,1);
}
int qq1(int x,int y){
int ans=0;
while(top[x]!=top[y]){
if(dis[top[x]]<dis[top[y]]) swap(x,y);
ans+=findl(1,n,dfn[top[x]],dfn[x],1);
ans%=mod;
x=f[top[x]];
}
if(dis[x]>dis[y]) swap(x,y);
ans+=findl(1,n,dfn[x],dfn[y],1);
return ans%mod;
}
void cc2(int x,int s){
add(1,n,dfn[x],dfn[x]+siz[x]-1,s,1);
}
int qq2(int x){
return findl(1,n,dfn[x],dfn[x]+siz[x]-1,1);
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>m>>rt>>mod;
for(int i=1;i<=n;i++) cin>>org[i];
for(int i=1;i<n;i++){
int x,y;
cin>>x>>y;
a[x].push_back(y);
a[y].push_back(x);
}
dfs1(rt,0);
dfs2(rt,0,rt);
build(1,n,1);
// cc2(1,1);
// cout<<dfn[3]<<" ";
// cout<<qq2(4);
while(m--){
int o;
cin>>o;
if(o==1){
int x,y,z;
cin>>x>>y>>z;
cc1(x,y,z%mod);
}else if(o==2){
int x,y;
cin>>x>>y;
cout<<qq1(x,y)<<'\n';
}else if(o==3){
int x,z;
cin>>x>>z;
cc2(x,z%mod);
}else{
int x;
cin>>x;
cout<<qq2(x)<<'\n';
}
}
}